Analyze possibility of incorporating vue-maplibre-gl #123

Open
opened 2026-07-16 15:28:17 +00:00 by eliribble · 1 comment
Owner

We're currently using our own custom single-file components for integrating maplibre-gl into our VueJS frontend. We're missing a lot of features because we've built it ourselves. I'm considering switching over to using vue-maplibre-gl. The documentation is pretty short, so I need a thorough analysis to determine if it has the features we need.

I'd like you to audit our current usage of maplibre-gl throughout the codebase. List all the different patterns that you find, and group them together by which parts of the system are using the different patterns. Then I'd like you to analyze what capabilities that vue-maplibre-gl has. See if you can create a mapping from our existing patterns to ways we can achieve the same behavior, but with maplibre-gl. This will serve as both a report to aid in making the decision, and as an implementation guide for engineers who will be doing the porting work.

We're currently using our own custom single-file components for integrating maplibre-gl into our VueJS frontend. We're missing a lot of features because we've built it ourselves. I'm considering switching over to using [vue-maplibre-gl](https://github.com/razorness/vue-maplibre-gl). The documentation is pretty short, so I need a thorough analysis to determine if it has the features we need. I'd like you to audit our current usage of maplibre-gl throughout the codebase. List all the different patterns that you find, and group them together by which parts of the system are using the different patterns. Then I'd like you to analyze what capabilities that vue-maplibre-gl has. See if you can create a mapping from our existing patterns to ways we can achieve the same behavior, but with maplibre-gl. This will serve as both a report to aid in making the decision, and as an implementation guide for engineers who will be doing the porting work.
Member

Audit of Current maplibre-gl Usage & vue-maplibre-gl Analysis

Overview

I've audited every file referencing maplibre-gl in the codebase. There are 6 distinct architectural patterns in use, spanning legacy vanilla-JS Web Components, custom Vue SFCs, and a newer component-composition approach. Below I group them by system area and pattern, then map each to vue-maplibre-gl equivalents.


Part 1: Current Usage Patterns by System Area

Pattern A: Legacy Vanilla JS Web Components (static/js/)

These are plain custom elements using Shadow DOM, injected directly into non-Vue pages or legacy views.

File Purpose
map-locator.js Single draggable marker for point location
map-locator-ro.js Read-only single marker display
map-cell.js Show a single H3 hex cell
map-admin.js Admin map with tegola vector sources + layers
map-routing.js Technician routing display (route lines, stops as circles, popup labels)
map-aggregate.js Aggregate data overlay on tegola tiles
map-multipoint.js Multiple markers with popup details
map-service-area.js Service area polygon fill + outline
map-arcgis-tile.js Raster ArcGIS tile layer overlay
map-proxied-arcgis-tile.js Proxied ArcGIS tile layer
address-suggestion.js Address autocomplete via Mapbox Geocoding (not map rendering)

Characteristics:

  • Shadow DOM, connectedCallbacksetTimeout(initializeMap, 0)
  • maplibregl.Map() with hard-coded Stadia style URLs
  • Manual addSource/addLayer in map.on('load', ...)
  • Custom attribute-based API (getAttribute('latitude'), getAttribute('centroid'), etc.)
  • Manual marker management
  • No reactive framework integration

Verdict: These won't benefit from vue-maplibre-gl (they're non-Vue). They'd need to be converted to Vue components or remain as-is.


Pattern B: Monolithic SFC Map Components (ts/components/Map*.vue)

Self-contained components that create and manage their own maplibregl.Map instance directly.

Group: Core Map Components

File Lines Purpose Used By
MapLocator.vue 550 Location picker with lock/unlock, draggable markers, camera model (v-model) RMO public pages
MapLocatorDisplay.vue 179 Read-only display of markers, read-only mode RMO public pages
MapAggregate.vue 198 Aggregate heat map with tegola vector sources Dash view
MapMultipoint.vue 189 Multiple markers + tegola service-area layer Planning view
MapProxiedArcgisTile.vue 428 Raster flyover imagery + tegola vector overlay + markers Review site views
MapServiceArea.vue 107 Service area polygon + optional satellite surveillance area Various
MapOperations.vue 270 Technician route display (GeoJSON line sources + circle layers + symbol labels + popups) Operations view

Characteristics:

  • Each component initializes its own new maplibregl.Map() independently
  • CSS imported either via @import url(...) from unpkg CDN or import "maplibre-gl/dist/maplibre-gl.css"
  • Manual addSource/addLayer in map.on('load', ...)
  • Manual marker lifecycle management (add/remove/update via mapMarkers Map)
  • Manual interaction controls (scrollZoom.disable(), lock/unlock pattern)
  • Camera model via v-model pattern (emit update:modelValue with Camera objects)
  • Custom isInternalUpdate flag on move/zoom events to prevent feedback loops
  • Bootstrap + custom CSS for overlays

Pattern C: Component Composition System (ts/map/)

A newer, more reusable pattern — renderless components using Vue provide/inject.

Core files:

  • Map.vue (355 lines) — provides map instance via provide('map', ...), plus registration functions for child components (registerSource, registerLayer, registerOn, registerOnce)
  • Layer.vue (123 lines) — renderless, registers itself via inject, handles events (click, mouseenter, mouseleave), watches paint/layout changes
  • Source.vue (49 lines) — renderless, registers vector/raster/geojson source config
  • SourceGeoJSON.vue — thin wrapper around Source with type="geojson" and :data prop
  • SourceTegola.vue — thin wrapper computing tegola tile URLs from session store
  • util.ts — utility functions (bounds helpers, marker bounds)

Used by views:

View Components Used
Dash.vue <Map>, <Source>, <Layer> (5 layers: mosquito_source, parcel, service_request, trap, service-area)
sudo/UI.vue <Map>, <SourceGeoJSON>, <Layer>
sudo/PlanetItemDetail.vue <Map>, <SourceGeoJSON>, <Layer> (fill + line)
sudo/PlanetDetail.vue <Map>
sudo/Parcel.vue <Map>
sudo/ParcelDetail.vue <Map>
sudo/SitusDetail.vue References map layout but not component composition
Cell.vue Imports <Map> but template is stub

Characteristics:

  • Nesting pattern: <Map><Source/><Layer/><Layer/>...</Map>
  • Sources and layers are renderless — they register themselves with the parent Map via injected callbacks
  • Event handling via registerOn/registerOnce injects
  • Reactivity: paint property changes watched via setPaintProperty
  • Limitation noticed: unregisterSource and unregisterLayer are commented out — cleanup on unmount is broken

Pattern D: LayersControl Custom IControl (ts/components/LayersControl.ts)

A standalone TypeScript class implementing maplibregl.IControl for toggling layer visibility with checkbox/radio/opacity controls. Based on maplibre-gl-layers-control.

Used by: MapProxiedArcgisTile.vue (currently commented out: _map.addControl(new LayersControl({...})))


Pattern E: Direct Imperative in View Scripts

Some view scripts import maplibregl directly for utility functions:

  • Communication.vue — imports maplibregl for boundsForServiceArea/boundsWithPadding from @/map/util
  • ts/types.ts — imports Map as MapLibreMap from maplibre-gl for type definitions

Pattern F: CSS Only

  • vite/sync/main.ts — imports "maplibre-gl/dist/maplibre-gl.css" globally
  • Several component <style> blocks import CSS via @import url(...) or @use "~maplibre-gl/dist/maplibre-gl.css"

Part 2: vue-maplibre-gl Capabilities

Package: razorness/vue-maplibre-gl v5.6.1 (npm)
Dependencies: maplibre-gl ^5.17.0, mitt ^3.0.1, vue ^3.5.27
Also installs peer: vue (already have)
Additional optional: Turf.js (for draw plugin)

Available Components

Map: <MglMap>
Props: Every MapOptions from maplibre-gl (center, zoom, bounds, stylemapStyle, projection, language, etc.) plus fitBoundsOptions, mapStyle, mapKey (for multi-map).
Events: All maplibre-gl events prefixed with map: (@map:load, @map:click, @map:moveend, @map:zoomend, etc.)
Slots: default (for child components)

Controls:

  • <MglNavigationControl> — zoom +/- buttons
  • <MglGeolocationControl> — locate user
  • <MglFullscreenControl> — fullscreen toggle
  • <MglScaleControl> — scale bar
  • <MglFrameRateControl> — FPS monitor
  • <MglStyleSwitchControl> — style picker
  • <MglAttributionControl> — attribution
  • <MglCustomControl> — custom button control

Sources (nestable inside MglMap):

  • <MglGeoJsonSource>source-id, data, clustering props
  • <MglVectorSource>source-id, tiles[], url, bounds, etc.
  • <MglRasterSource> — raster tile sources
  • <MglRasterDemSource> — DEM/elevation
  • <MglImageSource> — single image source
  • <MglCanvasSource> — canvas source
  • <MglVideoSource> — video source

Layers (nestable inside sources):

  • <MglFillLayer>layer-id, source, source-layer, paint, layout, filter
  • <MglLineLayer> — same pattern
  • <MglCircleLayer> — same pattern
  • <MglSymbolLayer> — same pattern, with text/icon props
  • <MglBackgroundLayer> — background
  • <MglHeatmapLayer> — heatmap
  • <MglHillshadeLayer> — hillshade
  • <MglRasterLayer> — raster
  • <MglFillExtrusionLayer> — 3D extrusion

Markers:

  • <MglMarker>coordinates, color, scale, offset, anchor, rotation, rotationAlignment, pitchAlignment
  • Note: draggable prop is listed in source but commented out (not exposed yet)

Draw Plugin (separate build):

  • <MglDrawControl> — draw polygons, circles, static circles; v-model for DrawModel
  • Requires Turf.js dependencies
  • Comes with its own CSS

Composable API:

  • useMap(key?) — get map instance and reactive state (isLoaded, isMounted, language)
  • MglDefaults — configure global defaults for the map

Key Features:

  • TypeScript support throughout
  • Automatic style switch with source/layer re-registration
  • WebGL context lost auto-restart (mobile-friendly)
  • Multi-map support with mapKey + useMap()
  • Language switching via language prop
  • Reactive source .setData() / .setTiles() / .setUrl() via watchers
  • Debounced resize observer
  • Source and layer cleanup on unmount

Part 3: Mapping — Current Patterns → vue-maplibre-gl Equivalents

3.1: Pattern C (Component Composition) — Best Fit

This is the cleanest migration path since it already uses renderless child components.

Current (our code):

<Map>
  <Source id="tegola" type="vector" :tiles="tegolaTiles" />
  <Layer id="mosquito_source" type="fill" source="tegola" sourceLayer="mosquito_source" :paint="paint" @click="onClick" />
</Map>

vue-maplibre-gl equivalent:

<MglMap :mapStyle="styleURL" :bounds="bounds" :center="center" :zoom="zoom" @map:load="onLoad">
  <MglVectorSource source-id="tegola" :tiles="tegolaTiles">
    <MglFillLayer layer-id="mosquito_source" source-layer="mosquito_source" :paint="paint" :filter="filter" @click="onClick" />
  </MglVectorSource>
</MglMap>

Key differences:

  • Map.vueMglMap (mapStyle prop instead of style)
  • SourceMglVectorSource/MglRasterSource/MglGeoJsonSource (prop idsource-id)
  • Layer → typed layers (MglFillLayer, MglLineLayer, MglCircleLayer, etc.) (prop idlayer-id, sourceLayersource-layer)
  • Events: @click@click (same, but layer components emit MapLayerMouseEvent directly)
  • Layer emissions on vue-maplibre-gl: @mouseenter, @mouseleave, @click from LayerLib shared
  • Sources now nest their layer children
  • Source change reactivity and cleanup are handled automatically

Affected views:

View Migration Complexity
Dash.vue Straightforward — 1 source, 5 layers, direct substitution
sudo/UI.vue Straightforward — 1 GeoJSON source, 1 fill layer
sudo/PlanetItemDetail.vue Straightforward — 1 GeoJSON source, 2 layers (fill + line)
sudo/PlanetDetail.vue Minimal — just <Map/> with no children

3.2: Pattern B (Monolithic SFCs) — Migration Path

Each of these components encapsulates specific interactive behaviors beyond just map display.

MapLocator.vue / MapLocatorDisplay.vue:
Current behavior: lock/unlock interaction, draggable markers, camera v-model, marker frame-on-add.

vue-maplibre-gl mapping:

maplibregl.Map → MglMap
  props: center, zoom, bounds → same MglMap props
  MapLocator's custom lock/unlock → MglMap has interactive, scrollZoom, dragPan props (can bind to toggle state)
  Markers → MglMarker (coordinates, color; no draggable yet though)
  Camera model → combine @map:moveend + @map:zoomend into v-model
  NavigationControl → MglNavigationControl
  fitBounds / panTo → use map ref from useMap() key

Blockers: MglMarker doesn't expose draggable prop currently. Would need a custom wrapper or to contribute upstream.

MapProxiedArcgisTile.vue:
Current: vector (tegola) + raster (flyover) sources + markers + NavigationControl.

vue-maplibre-gl mapping:

maplibregl.Map → MglMap
  addSource('tegola', vector) → MglVectorSource
  addLayer('service-area', line) → MglLineLayer nested in source
  addSource('flyover', raster) → MglRasterSource
  addLayer('flyover-layer', raster) → MglRasterLayer
  markers → MglMarker
  NavigationControl → MglNavigationControl
  click handler → @map:click
  state management (@map:load → isLoaded) → built-in useMap().isLoaded

MapServiceArea.vue:
Current: GeoJSON service area + satellite surveillance area.

vue-maplibre-gl mapping:

MglMap
  MglGeoJsonSource(source-id="service-area", :data="serviceArea")
    MglFillLayer(layer-id="service-area-fill")
    MglLineLayer(layer-id="service-area-outline")
  MglGeoJsonSource(source-id="satellite-surveillance-area", :data="satelliteSurveillanceArea") (if present)
    MglFillLayer(...)
    MglLineLayer(...)

MapOperations.vue:
Current: 3 GeoJSON line sources for routes, 3 stop-point sources, circle layers, symbol layers with labels, popups on click.

vue-maplibre-gl mapping:

MglMap
  → route-a:
    MglGeoJsonSource(source-id="route-a", :data="lineStringGeoJSON")
      MglLineLayer(layer-id="route-a", :paint="...")
    MglGeoJsonSource(source-id="route-a-stops", :data="stopPointsGeoJSON")
      MglCircleLayer(layer-id="route-a-stops", :paint="...")
      MglSymbolLayer(layer-id="route-a-labels", layout: { text-field: ['get', 'stopNumber'] })
  → route-b, route-c: same pattern
  popups → @click on circle layers, use maplibregl.Popup directly
  NavigationControl → MglNavigationControl

MapAggregate.vue / MapMultipoint.vue:
Similar pattern — tegola vector source + fill layers + markers or click handling.

Consideration for Pattern B: These are more tightly coupled to their domain logic. A phased migration would extract the map initialization into MglMap first, then replace sources/layers piece by piece.

3.3: Pattern A (Vanilla JS Web Components) — Not Migrating

These live outside Vue entirely. They would need full rewrite to Vue components to benefit from vue-maplibre-gl. Not recommended to convert unless those pages are being migrated to Vue anyway.

3.4: Pattern D (LayersControl) — Replace with MglStyleSwitchControl?

The custom LayersControl provides checkbox/radio toggle + opacity. MglStyleSwitchControl is for switching full map styles (not individual layers). If layer visibility toggling is needed, could implement with a custom component using useMap() to access the map instance directly.

3.5: Pattern E (Direct Imperative) — No Change Needed

Utility imports are backward-compatible since vue-maplibre-gl is a wrapper, not a replacement of maplibre-gl. import maplibregl from "maplibre-gl" continues to work.


Part 4: Feature Gap Analysis

Feature We Use vue-maplibre-gl Support Notes
Stadia Maps style URLs Via mapStyle prop Can set MglDefaults.style = "..." or per-instance
Tegola vector tile sources MglVectorSource with tiles prop Nest layers inside
Raster tile sources (flyover/ArcGIS) MglRasterSource with tiles prop
GeoJSON sources (inline data) MglGeoJsonSource with data prop Reactive .setData() via watcher
Fill layers MglFillLayer
Line layers MglLineLayer
Circle layers (route stops) MglCircleLayer
Symbol layers (labels) MglSymbolLayer
Markers (static) MglMarker color, scale, offset, etc.
Markers (draggable) Not exposed draggable prop exists in source model but commented out in component. Could fork or contribute.
Markers (custom HTML element) Custom via CSS MglMarker has color prop, but custom element would need a different approach
Navigation controls MglNavigationControl
Style switcher MglStyleSwitchControl Built-in, with auto source/layer re-registration
Camera model (v-model) Manual Combine @map:moveend + @map:zoomend + reactive center/zoom props
Bounds fitting on load fitBoundsOptions prop, :bounds prop useOnBoundsUpdate option
Layer visibility toggles Partial Would need custom implementation on top of useMap()
Layer opacity control Not built-in Custom implementation needed
Interactive lock/unlock Via :interactive prop or :scrollZoom/:dragPan props Much cleaner than current imperative enable/disable
Geocoding/address Not built-in Use separate address autocomplete solution
WebGL context auto-restart Built-in

Part 5: Decision Support Summary

Strengths of vue-maplibre-gl:

  1. Clean declarative API — especially for the Pattern C views (Dash, sudo/*), migration is almost 1:1
  2. Automatic cleanup — sources, layers, and events are properly disposed (fixes the commented-out cleanup in our current system)
  3. TypeScript support — matches our codebase
  4. Reactive props — paint, layout, data watchers built in
  5. Style switch with persistence — sources/layers survive style changes
  6. Active maintenance — v5.6.1, supports maplibre-gl v5.x (which we're on)
  7. Multi-map instancesuseMap(key) for apps with multiple maps

Weaknesses / Gaps:

  1. No draggable markers — need a workaround or upstream contribution
  2. No built-in layer visibility control panel — our custom LayersControl would need reimplementation
  3. Custom HTML markers — can't easily replace our custom styled elements
  4. Small documentation — the README is minimal; the dev/App.vue is the real documentation
  5. No draggable marker emitted event for drag-end — our MapLocator relies on this

Recommendation:
The component composition pattern (Pattern C — Dash.vue, sudo/*) should be the first migration target since it's already architecturally aligned. The monolithic SFCs (Pattern B) are higher effort but still feasible in phases. The legacy Web Components (Pattern A) can be left as-is or converted when those pages are modernized.

## Audit of Current maplibre-gl Usage & vue-maplibre-gl Analysis ### Overview I've audited every file referencing maplibre-gl in the codebase. There are **6 distinct architectural patterns** in use, spanning legacy vanilla-JS Web Components, custom Vue SFCs, and a newer component-composition approach. Below I group them by system area and pattern, then map each to vue-maplibre-gl equivalents. --- ### Part 1: Current Usage Patterns by System Area #### Pattern A: Legacy Vanilla JS Web Components (static/js/) These are plain custom elements using Shadow DOM, injected directly into non-Vue pages or legacy views. | File | Purpose | |---|---| | `map-locator.js` | Single draggable marker for point location | | `map-locator-ro.js` | Read-only single marker display | | `map-cell.js` | Show a single H3 hex cell | | `map-admin.js` | Admin map with tegola vector sources + layers | | `map-routing.js` | Technician routing display (route lines, stops as circles, popup labels) | | `map-aggregate.js` | Aggregate data overlay on tegola tiles | | `map-multipoint.js` | Multiple markers with popup details | | `map-service-area.js` | Service area polygon fill + outline | | `map-arcgis-tile.js` | Raster ArcGIS tile layer overlay | | `map-proxied-arcgis-tile.js` | Proxied ArcGIS tile layer | | `address-suggestion.js` | Address autocomplete via Mapbox Geocoding (not map rendering) | **Characteristics:** - Shadow DOM, `connectedCallback` → `setTimeout(initializeMap, 0)` - `maplibregl.Map()` with hard-coded Stadia style URLs - Manual `addSource`/`addLayer` in `map.on('load', ...)` - Custom attribute-based API (`getAttribute('latitude')`, `getAttribute('centroid')`, etc.) - Manual marker management - No reactive framework integration **Verdict:** These won't benefit from vue-maplibre-gl (they're non-Vue). They'd need to be converted to Vue components or remain as-is. --- #### Pattern B: Monolithic SFC Map Components (ts/components/Map*.vue) Self-contained components that create and manage their own `maplibregl.Map` instance directly. **Group: Core Map Components** | File | Lines | Purpose | Used By | |---|---|---|---| | `MapLocator.vue` | 550 | Location picker with lock/unlock, draggable markers, camera model (v-model) | RMO public pages | | `MapLocatorDisplay.vue` | 179 | Read-only display of markers, read-only mode | RMO public pages | | `MapAggregate.vue` | 198 | Aggregate heat map with tegola vector sources | Dash view | | `MapMultipoint.vue` | 189 | Multiple markers + tegola service-area layer | Planning view | | `MapProxiedArcgisTile.vue` | 428 | Raster flyover imagery + tegola vector overlay + markers | Review site views | | `MapServiceArea.vue` | 107 | Service area polygon + optional satellite surveillance area | Various | | `MapOperations.vue` | 270 | Technician route display (GeoJSON line sources + circle layers + symbol labels + popups) | Operations view | **Characteristics:** - Each component initializes its own `new maplibregl.Map()` independently - CSS imported either via `@import url(...)` from unpkg CDN or `import "maplibre-gl/dist/maplibre-gl.css"` - Manual `addSource`/`addLayer` in `map.on('load', ...)` - Manual marker lifecycle management (add/remove/update via `mapMarkers` Map) - Manual interaction controls (scrollZoom.disable(), lock/unlock pattern) - Camera model via v-model pattern (emit `update:modelValue` with `Camera` objects) - Custom `isInternalUpdate` flag on move/zoom events to prevent feedback loops - Bootstrap + custom CSS for overlays --- #### Pattern C: Component Composition System (ts/map/) A newer, more reusable pattern — renderless components using Vue `provide/inject`. **Core files:** - `Map.vue` (355 lines) — provides `map` instance via `provide('map', ...)`, plus registration functions for child components (`registerSource`, `registerLayer`, `registerOn`, `registerOnce`) - `Layer.vue` (123 lines) — renderless, registers itself via inject, handles events (click, mouseenter, mouseleave), watches paint/layout changes - `Source.vue` (49 lines) — renderless, registers vector/raster/geojson source config - `SourceGeoJSON.vue` — thin wrapper around `Source` with `type="geojson"` and `:data` prop - `SourceTegola.vue` — thin wrapper computing tegola tile URLs from session store - `util.ts` — utility functions (bounds helpers, marker bounds) **Used by views:** | View | Components Used | |---|---| | `Dash.vue` | `<Map>`, `<Source>`, `<Layer>` (5 layers: mosquito_source, parcel, service_request, trap, service-area) | | `sudo/UI.vue` | `<Map>`, `<SourceGeoJSON>`, `<Layer>` | | `sudo/PlanetItemDetail.vue` | `<Map>`, `<SourceGeoJSON>`, `<Layer>` (fill + line) | | `sudo/PlanetDetail.vue` | `<Map>` | | `sudo/Parcel.vue` | `<Map>` | | `sudo/ParcelDetail.vue` | `<Map>` | | `sudo/SitusDetail.vue` | References map layout but not component composition | | `Cell.vue` | Imports `<Map>` but template is stub | **Characteristics:** - **Nesting pattern:** `<Map><Source/><Layer/><Layer/>...</Map>` - Sources and layers are renderless — they register themselves with the parent `Map` via injected callbacks - Event handling via `registerOn`/`registerOnce` injects - Reactivity: paint property changes watched via `setPaintProperty` - **Limitation noticed:** `unregisterSource` and `unregisterLayer` are **commented out** — cleanup on unmount is broken --- #### Pattern D: LayersControl Custom IControl (ts/components/LayersControl.ts) A standalone TypeScript class implementing `maplibregl.IControl` for toggling layer visibility with checkbox/radio/opacity controls. Based on `maplibre-gl-layers-control`. **Used by:** `MapProxiedArcgisTile.vue` (currently commented out: `_map.addControl(new LayersControl({...}))`) --- #### Pattern E: Direct Imperative in View Scripts Some view scripts import maplibregl directly for utility functions: - `Communication.vue` — imports `maplibregl` for `boundsForServiceArea`/`boundsWithPadding` from `@/map/util` - `ts/types.ts` — imports `Map as MapLibreMap` from maplibre-gl for type definitions --- #### Pattern F: CSS Only - `vite/sync/main.ts` — imports `"maplibre-gl/dist/maplibre-gl.css"` globally - Several component `<style>` blocks import CSS via `@import url(...)` or `@use "~maplibre-gl/dist/maplibre-gl.css"` --- ### Part 2: vue-maplibre-gl Capabilities **Package:** `razorness/vue-maplibre-gl` v5.6.1 (npm) **Dependencies:** maplibre-gl ^5.17.0, mitt ^3.0.1, vue ^3.5.27 **Also installs peer:** vue (already have) **Additional optional:** Turf.js (for draw plugin) #### Available Components **Map:** `<MglMap>` Props: Every `MapOptions` from maplibre-gl (`center`, `zoom`, `bounds`, `style` → `mapStyle`, `projection`, `language`, etc.) plus `fitBoundsOptions`, `mapStyle`, `mapKey` (for multi-map). Events: All maplibre-gl events prefixed with `map:` (`@map:load`, `@map:click`, `@map:moveend`, `@map:zoomend`, etc.) Slots: default (for child components) **Controls:** - `<MglNavigationControl>` — zoom +/- buttons - `<MglGeolocationControl>` — locate user - `<MglFullscreenControl>` — fullscreen toggle - `<MglScaleControl>` — scale bar - `<MglFrameRateControl>` — FPS monitor - `<MglStyleSwitchControl>` — style picker - `<MglAttributionControl>` — attribution - `<MglCustomControl>` — custom button control **Sources (nestable inside MglMap):** - `<MglGeoJsonSource>` — `source-id`, `data`, clustering props - `<MglVectorSource>` — `source-id`, `tiles`[], `url`, `bounds`, etc. - `<MglRasterSource>` — raster tile sources - `<MglRasterDemSource>` — DEM/elevation - `<MglImageSource>` — single image source - `<MglCanvasSource>` — canvas source - `<MglVideoSource>` — video source **Layers (nestable inside sources):** - `<MglFillLayer>` — `layer-id`, `source`, `source-layer`, `paint`, `layout`, `filter` - `<MglLineLayer>` — same pattern - `<MglCircleLayer>` — same pattern - `<MglSymbolLayer>` — same pattern, with text/icon props - `<MglBackgroundLayer>` — background - `<MglHeatmapLayer>` — heatmap - `<MglHillshadeLayer>` — hillshade - `<MglRasterLayer>` — raster - `<MglFillExtrusionLayer>` — 3D extrusion **Markers:** - `<MglMarker>` — `coordinates`, `color`, `scale`, `offset`, `anchor`, `rotation`, `rotationAlignment`, `pitchAlignment` - Note: `draggable` prop is listed in source but commented out (not exposed yet) **Draw Plugin (separate build):** - `<MglDrawControl>` — draw polygons, circles, static circles; `v-model` for `DrawModel` - Requires Turf.js dependencies - Comes with its own CSS **Composable API:** - `useMap(key?)` — get map instance and reactive state (`isLoaded`, `isMounted`, `language`) - `MglDefaults` — configure global defaults for the map **Key Features:** - TypeScript support throughout - Automatic style switch with source/layer re-registration - WebGL context lost auto-restart (mobile-friendly) - Multi-map support with `mapKey` + `useMap()` - Language switching via `language` prop - Reactive source `.setData()` / `.setTiles()` / `.setUrl()` via watchers - Debounced resize observer - Source and layer cleanup on unmount --- ### Part 3: Mapping — Current Patterns → vue-maplibre-gl Equivalents #### 3.1: Pattern C (Component Composition) — Best Fit This is the cleanest migration path since it already uses renderless child components. **Current (our code):** ```vue <Map> <Source id="tegola" type="vector" :tiles="tegolaTiles" /> <Layer id="mosquito_source" type="fill" source="tegola" sourceLayer="mosquito_source" :paint="paint" @click="onClick" /> </Map> ``` **vue-maplibre-gl equivalent:** ```vue <MglMap :mapStyle="styleURL" :bounds="bounds" :center="center" :zoom="zoom" @map:load="onLoad"> <MglVectorSource source-id="tegola" :tiles="tegolaTiles"> <MglFillLayer layer-id="mosquito_source" source-layer="mosquito_source" :paint="paint" :filter="filter" @click="onClick" /> </MglVectorSource> </MglMap> ``` **Key differences:** - `Map.vue` → `MglMap` (mapStyle prop instead of `style`) - `Source` → `MglVectorSource`/`MglRasterSource`/`MglGeoJsonSource` (prop `id` → `source-id`) - `Layer` → typed layers (`MglFillLayer`, `MglLineLayer`, `MglCircleLayer`, etc.) (prop `id` → `layer-id`, `sourceLayer` → `source-layer`) - Events: `@click` → `@click` (same, but layer components emit MapLayerMouseEvent directly) - Layer emissions on vue-maplibre-gl: `@mouseenter`, `@mouseleave`, `@click` from LayerLib shared - Sources now nest their layer children - Source change reactivity and cleanup are handled automatically **Affected views:** | View | Migration Complexity | |---|---| | `Dash.vue` | **Straightforward** — 1 source, 5 layers, direct substitution | | `sudo/UI.vue` | **Straightforward** — 1 GeoJSON source, 1 fill layer | | `sudo/PlanetItemDetail.vue` | **Straightforward** — 1 GeoJSON source, 2 layers (fill + line) | | `sudo/PlanetDetail.vue` | **Minimal** — just `<Map/>` with no children | #### 3.2: Pattern B (Monolithic SFCs) — Migration Path Each of these components encapsulates specific interactive behaviors beyond just map display. **MapLocator.vue / MapLocatorDisplay.vue:** Current behavior: lock/unlock interaction, draggable markers, camera v-model, marker frame-on-add. vue-maplibre-gl mapping: ``` maplibregl.Map → MglMap props: center, zoom, bounds → same MglMap props MapLocator's custom lock/unlock → MglMap has interactive, scrollZoom, dragPan props (can bind to toggle state) Markers → MglMarker (coordinates, color; no draggable yet though) Camera model → combine @map:moveend + @map:zoomend into v-model NavigationControl → MglNavigationControl fitBounds / panTo → use map ref from useMap() key ``` **Blockers:** `MglMarker` doesn't expose `draggable` prop currently. Would need a custom wrapper or to contribute upstream. **MapProxiedArcgisTile.vue:** Current: vector (tegola) + raster (flyover) sources + markers + NavigationControl. vue-maplibre-gl mapping: ``` maplibregl.Map → MglMap addSource('tegola', vector) → MglVectorSource addLayer('service-area', line) → MglLineLayer nested in source addSource('flyover', raster) → MglRasterSource addLayer('flyover-layer', raster) → MglRasterLayer markers → MglMarker NavigationControl → MglNavigationControl click handler → @map:click state management (@map:load → isLoaded) → built-in useMap().isLoaded ``` **MapServiceArea.vue:** Current: GeoJSON service area + satellite surveillance area. vue-maplibre-gl mapping: ``` MglMap MglGeoJsonSource(source-id="service-area", :data="serviceArea") MglFillLayer(layer-id="service-area-fill") MglLineLayer(layer-id="service-area-outline") MglGeoJsonSource(source-id="satellite-surveillance-area", :data="satelliteSurveillanceArea") (if present) MglFillLayer(...) MglLineLayer(...) ``` **MapOperations.vue:** Current: 3 GeoJSON line sources for routes, 3 stop-point sources, circle layers, symbol layers with labels, popups on click. vue-maplibre-gl mapping: ``` MglMap → route-a: MglGeoJsonSource(source-id="route-a", :data="lineStringGeoJSON") MglLineLayer(layer-id="route-a", :paint="...") MglGeoJsonSource(source-id="route-a-stops", :data="stopPointsGeoJSON") MglCircleLayer(layer-id="route-a-stops", :paint="...") MglSymbolLayer(layer-id="route-a-labels", layout: { text-field: ['get', 'stopNumber'] }) → route-b, route-c: same pattern popups → @click on circle layers, use maplibregl.Popup directly NavigationControl → MglNavigationControl ``` **MapAggregate.vue / MapMultipoint.vue:** Similar pattern — tegola vector source + fill layers + markers or click handling. **Consideration for Pattern B:** These are more tightly coupled to their domain logic. A phased migration would extract the map initialization into MglMap first, then replace sources/layers piece by piece. #### 3.3: Pattern A (Vanilla JS Web Components) — Not Migrating These live outside Vue entirely. They would need full rewrite to Vue components to benefit from vue-maplibre-gl. Not recommended to convert unless those pages are being migrated to Vue anyway. #### 3.4: Pattern D (LayersControl) — Replace with MglStyleSwitchControl? The custom `LayersControl` provides checkbox/radio toggle + opacity. `MglStyleSwitchControl` is for switching full map styles (not individual layers). If layer visibility toggling is needed, could implement with a custom component using `useMap()` to access the map instance directly. #### 3.5: Pattern E (Direct Imperative) — No Change Needed Utility imports are backward-compatible since vue-maplibre-gl is a wrapper, not a replacement of maplibre-gl. `import maplibregl from "maplibre-gl"` continues to work. --- ### Part 4: Feature Gap Analysis | Feature We Use | vue-maplibre-gl Support | Notes | |---|---|---| | Stadia Maps style URLs | ✅ Via `mapStyle` prop | Can set `MglDefaults.style = "..."` or per-instance | | Tegola vector tile sources | ✅ `MglVectorSource` with `tiles` prop | Nest layers inside | | Raster tile sources (flyover/ArcGIS) | ✅ `MglRasterSource` with `tiles` prop | | | GeoJSON sources (inline data) | ✅ `MglGeoJsonSource` with `data` prop | Reactive `.setData()` via watcher | | Fill layers | ✅ `MglFillLayer` | | | Line layers | ✅ `MglLineLayer` | | | Circle layers (route stops) | ✅ `MglCircleLayer` | | | Symbol layers (labels) | ✅ `MglSymbolLayer` | | | Markers (static) | ✅ `MglMarker` | color, scale, offset, etc. | | Markers (draggable) | ❌ Not exposed | `draggable` prop exists in source model but commented out in component. Could fork or contribute. | | Markers (custom HTML element) | ➖ Custom via CSS | `MglMarker` has `color` prop, but custom element would need a different approach | | Navigation controls | ✅ `MglNavigationControl` | | | Style switcher | ✅ `MglStyleSwitchControl` | Built-in, with auto source/layer re-registration | | Camera model (v-model) | ➖ Manual | Combine `@map:moveend` + `@map:zoomend` + reactive `center`/`zoom` props | | Bounds fitting on load | ✅ `fitBoundsOptions` prop, `:bounds` prop | `useOnBoundsUpdate` option | | Layer visibility toggles | ➖ Partial | Would need custom implementation on top of `useMap()` | | Layer opacity control | ❌ Not built-in | Custom implementation needed | | Interactive lock/unlock | ✅ Via `:interactive` prop or `:scrollZoom`/`:dragPan` props | Much cleaner than current imperative enable/disable | | Geocoding/address | ❌ Not built-in | Use separate address autocomplete solution | | WebGL context auto-restart | ✅ Built-in | | ### Part 5: Decision Support Summary **Strengths of vue-maplibre-gl:** 1. **Clean declarative API** — especially for the Pattern C views (Dash, sudo/*), migration is almost 1:1 2. **Automatic cleanup** — sources, layers, and events are properly disposed (fixes the commented-out cleanup in our current system) 3. **TypeScript support** — matches our codebase 4. **Reactive props** — paint, layout, data watchers built in 5. **Style switch with persistence** — sources/layers survive style changes 6. **Active maintenance** — v5.6.1, supports maplibre-gl v5.x (which we're on) 7. **Multi-map instances** — `useMap(key)` for apps with multiple maps **Weaknesses / Gaps:** 1. **No draggable markers** — need a workaround or upstream contribution 2. **No built-in layer visibility control panel** — our custom LayersControl would need reimplementation 3. **Custom HTML markers** — can't easily replace our custom styled elements 4. **Small documentation** — the README is minimal; the dev/App.vue is the real documentation 5. **No draggable marker emitted event for drag-end** — our MapLocator relies on this **Recommendation:** The component composition pattern (Pattern C — `Dash.vue`, `sudo/*`) should be the **first migration target** since it's already architecturally aligned. The monolithic SFCs (Pattern B) are higher effort but still feasible in phases. The legacy Web Components (Pattern A) can be left as-is or converted when those pages are modernized.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Gleipnir/nidus-sync#123
No description provided.