Skip to content

Expose core to plugins via engine-agnostic map:* APIs#75

Open
BhattaraiSijan wants to merge 21 commits into
developmentfrom
feat/add-basemap-to-leaflet
Open

Expose core to plugins via engine-agnostic map:* APIs#75
BhattaraiSijan wants to merge 21 commits into
developmentfrom
feat/add-basemap-to-leaflet

Conversation

@BhattaraiSijan
Copy link
Copy Markdown
Collaborator

Main change

Prepare the core to listen to plugins through the mmgisAPI Event Bus. New providers and emits are registered in Map_.js and routed through Map_.engine.* so Leaflet and deck.gl behave identically.

API Purpose
addOverlay / removeOverlay / clearOverlays Ephemeral GeoJSON overlays (not persisted, not in Layers tool)
setBasemap / getBasemap / getBasemapStyles Runtime basemap switching
zoomIn / zoomOut Camera stepping
on('map:click' | 'map:mousemove', h) Pointer events. Payload: {lat, lng, latlng, containerPoint?}

src/essence/Ancillary/BasemapSwitcher.js (jQuery UI) is removed.

Adapter changes

  • DeckGLAdapter.setBasemapStyle() — didn't exist; added for parity.
  • DeckGLAdapter._emitClick / _emitMouseMove — deck.gl's onClick / onHover only invoked internal feature handlers, so on('click' | 'mousemove', …) subscribers never fired. Added bridges normalizing PickingInfo to the same shape LeafletAdapter._normalizeEvent emits (including latlng so legacy consumers like Ancillary/Coordinates.js work unchanged on deck.gl).
  • DeckGLHelpers case 'vector' — deck.gl accepted only its native style keys (strokeColor/strokeWidth as RGBA). Leaflet's adapter accepts Leaflet's native vocab (color/weight/fillColor as CSS) directly. Made both adapters accept the same style dictionary via a _toRgba parser and Leaflet→deck.gl key aliases (deck.gl-native wins when both given). Also fixed a latent bug — deck.gl's default point radius is in meters, sub-pixel at typical zooms — via getPointRadius + pointRadiusUnits: 'pixels'.

Leaflet basemap via native L.tileLayer

The Leaflet engine now supports msv.basemap.provider = 'mapbox' | 'maplibre' (previously deck.gl-only). An earlier iteration mounted a MapLibre/Mapbox GL Map behind a transparent Leaflet with event-based camera sync — two render loops and a 256-vs-512 tile-size mismatch caused overlay drift and mis-projected clicks (tools recording coordinates saved wrong data).

Replaced with native L.tileLayer. _resolveBasemapTileSpec translates mapbox://styles/... → Mapbox Static Tiles API, raw {z}/{x}/{y} templates → passthrough, anything else → OSM fallback. One render loop, one projection.

Other

  • minZoom: 1 on Mapbox tile layers. Mapbox's 512-sized Static Tiles + zoomOffset: -1 means Leaflet zoom 0 resolves to URL zoom -1, which doesn't exist. Map minZoom bumped to match.
  • _resolveBasemapStyles(config, engineType) returns engine-appropriate defaults — Leaflet gets raster templates, deck.gl gets MapLibre vector style.json URLs. Same user-facing names on both (Streets / Light / Dark; Leaflet adds Terrain).
  • 'none' added to BasemapProvider union, removing a pre-existing as any.
  • NewMissionModal + tab-ui-config updated to surface the basemap option for Leaflet missions.

Test plan

Until the companion plugin PR lands, exercise the APIs from the browser console on a running mission via window.mmgisAPI.*. For example:

await window.mmgisAPI.addOverlay({
    id: 'sanity',
    geojson: { type: 'Feature', geometry: { type: 'LineString', coordinates: [[-74, 40], [-73, 41]] } },
    style: { color: '#1a73e8', weight: 3 },
})

Verify on each matrix cell (Leaflet + Mapbox, Leaflet + MapLibre, deck.gl + Mapbox, deck.gl + MapLibre):

  • Overlay renders as a visible 3px blue line.
  • setBasemap(name) swaps styles; zoomIn / zoomOut step and clamp.
  • on('map:click' | 'map:mousemove', …) fires with {lat, lng, latlng, containerPoint}.
  • removeOverlay / clearOverlays remove cleanly.

tileSize: 512,
zoomOffset: -1,
minZoom: 1,
attribution: '© Mapbox © OpenStreetMap',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to extract these into shared constants/defaults? Mainly to avoid scattering hardcoded values (tileSize, zoomOffset, attribution strings) and magic numbers/strings

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense @dzole0311 . Thanks.
For now, I just added those constants in the helper and imported them in the adapter.
We should discuss about maybe storing all such engine specific constants to some engine specific configuration files.

setBasemapStyle(styleUrl: string): void {
if (!this._map) return
const spec = this._resolveBasemapTileSpec({
provider: this._inferProvider(styleUrl),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is provider actually used here? From this flow it looks like the resolution is done by styleUrl alone so wondering if provider is redundant

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is redundant here. Thanks @dzole0311 .

As leaflet adapter does not actually spin up mapbox-gl or maplibre-gl based on provider, instead leafletadapter just uses L.tileLayer for both, I just ommitted the provider from the type setBasemapStyle

Comment thread configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js Outdated

const config = {
msv: {
view: [39, -98, 4],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this view be hard coded? This is not limited to earth so doesn't make sense to have it center on USA.
Also, I created new mission and it is still initializing to 0, 0, 0 so this might not be working as intended.

"field": "msv.basemap.provider",
"name": "Provider",
"description": "Renders a vector-tile basemap beneath deck.gl layers using MapboxOverlay. Has no effect on Leaflet-engine missions. 'maplibre' is open-source and requires no token. 'mapbox' requires an access token.",
"description": "Renders a vector-tile basemap beneath map layers using MapLibre or Mapbox GL. Works with both Leaflet and deck.gl engine missions. 'maplibre' is open-source and requires no token. 'mapbox' requires an access token.",
Copy link
Copy Markdown
Collaborator

@sandesh-sp sandesh-sp May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to mention that it works with both Leaflet and deck.gl? Might make more sense to only specify if something is engine specific or UI specific

]

const mapboxDefaults = MAPBOX_DEFAULTS
const maplibreDefaults = isLeaflet ? MAPLIBRE_DEFAULTS_LEAFLET : MAPLIBRE_DEFAULTS_DECKGL
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to assume we'll only have leaflet or deckgl? or should we add explicit checks?

{ name: 'Light', style: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png' },
{ name: 'Dark', style: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png' },
{ name: 'Terrain', style: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png' },
]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these styles be hardcoded? Might be difficult to change/update things in future.

Copy link
Copy Markdown
Collaborator

@sandesh-sp sandesh-sp May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be the case where different style might need different access tokens

? [...basemapConfig.styles]
: basemapConfig.provider === 'mapbox'
? [...mapboxDefaults]
: [...maplibreDefaults]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the logic correctly, it'll use the styles from mission config and fall back to default values if basemap isn't provided.
If that's the case, we need a way to provide multiple styles in the mission config page and a field to mark default style.
One potential issue might be when we have something other than earth. Right now, leaflet doesn't render any basemap so user can just add other planet/satellite as tile layer. This need not be addressed right away but should be a simple fix.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have found out the root of confusion, there's basemapConfig.styles to store all the styles and basemapConfig.style to store current style. I believe there might be better way to handle this.
One potential drawback will be the need to update both .style and .styles if there's change in name resulting in inconsistency.

Map_.engine.setZoom(next)
return true
}),
window.mmgisAPI.provide('map:addOverlay', ({ id, geojson, style } = {}) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the geojson already validated before being passed here?

return true
}),
window.mmgisAPI.provide('map:removeOverlay', (id) => {
if (!Map_.engine || !_overlayIds.has(id)) return false
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will it be helpful if we log error (here and all other functions) when the action fails?

* on top of a MapLibre/Mapbox GL basemap.
*
* **Leaflet**: Creates a MapLibre/Mapbox GL map behind a transparent Leaflet canvas
* and synchronises pan/zoom events so the basemap and Leaflet layers stay aligned.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we not scrapped this idea and instead decided on having basemap as one of the layer in leaflet?

* Optional vector-tile basemap to render beneath map layers.
*
* - **deck.gl**: renders via `MapboxOverlay` (overlay mode).
* - **Leaflet**: renders a MapLibre/Mapbox GL map behind a transparent Leaflet canvas.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, will we have mapbox beneath leaflet or will we use a raster layer as basemap?

*
* @throws {Error} If `options.type` is not a supported layer type.
*/
function _toRgba(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I don't see this being used anywhere
  2. This is not specific to deck.gl so even if this is needed, it can be moved from DeckGLHelpers to some place generic
  3. The comment above this function is for buildDeckLayer and should be moved

}
}

setBasemapStyle(styleUrl: string): void {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to respect basemap's minZoom setting when making the switch?

// BASEMAP TILE LAYER METHODS
// ========================================

private _initBasemapTileLayer(basemap: BasemapOptions): void {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this call setBasemapStyle for setting the style? I can see quite a bit of overlap between the two

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants