Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2718d32
leaflet basemap rendering
BhattaraiSijan Apr 14, 2026
524ff46
basemap style switcher fixed
BhattaraiSijan Apr 14, 2026
4616a20
basemap switcher APIs setup
BhattaraiSijan Apr 20, 2026
b6b2837
zoom apis added
BhattaraiSijan Apr 21, 2026
0694875
APIs for measure tool
BhattaraiSijan Apr 22, 2026
54bb007
route overlay + click APIs through engine adapter
BhattaraiSijan Apr 22, 2026
b7f2934
basemap as tilelayer
BhattaraiSijan Apr 22, 2026
319533b
fix:style urls in maplibre
BhattaraiSijan Apr 22, 2026
7ffc66f
changing style urls
BhattaraiSijan Apr 22, 2026
cd67ceb
maplibre dark style change
BhattaraiSijan Apr 22, 2026
036be3d
fix:min zoom set in mapbox leaflet
BhattaraiSijan Apr 22, 2026
dda2fe9
mouseover event emit, for draw
BhattaraiSijan Apr 22, 2026
a3b1a3d
fix(deck.gl): normalize Leaflet-style keys in vector layers
BhattaraiSijan Apr 22, 2026
cbc4536
fix(deck.gl): include latlng in normalized pointer events
BhattaraiSijan Apr 22, 2026
ae72021
clean up
BhattaraiSijan Apr 23, 2026
2eb8c4e
revert: drop default pointToLayer from vector layer builder
BhattaraiSijan Apr 23, 2026
748c51b
extract Mapbox/OSM tile defaults into LeafletHelpers constants
BhattaraiSijan Apr 28, 2026
0f23b30
Merge branch 'development' into feat/add-basemap-to-leaflet
BhattaraiSijan Apr 28, 2026
51485e0
refactor(LeafletAdapter): remove redundant basemap provider inference
BhattaraiSijan Apr 28, 2026
1a21746
Merge branch 'feat/add-basemap-to-leaflet' of https://github.com/NASA…
BhattaraiSijan Apr 28, 2026
5fd1d93
fix(Configure): remove empty props destructuring in NewMissionModal
BhattaraiSijan Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ const MAP_ENGINES = [

const MODAL_NAME = "newMission";
const NewMissionModal = (props) => {
const {} = props;
const c = useStyles();

const modal = useSelector((state) => state.core.modal[MODAL_NAME]);
Expand Down Expand Up @@ -155,6 +154,7 @@ const NewMissionModal = (props) => {

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.

radius: {
major: planetRadius.major,
minor: planetRadius.minor,
Expand All @@ -163,7 +163,7 @@ const NewMissionModal = (props) => {
},
};

if (selectedEngine === "deckgl" && basemapProvider !== "none" && basemapStyle) {
if (basemapProvider !== "none" && basemapStyle) {
config.msv.basemap = {
provider: basemapProvider,
style: basemapStyle,
Expand Down Expand Up @@ -313,7 +313,7 @@ const NewMissionModal = (props) => {
<Typography className={c.subtitle2}>
{`Choose the rendering engine for this mission's 2D map. This cannot be changed after the mission is created.`}
</Typography>
{selectedEngine === "deckgl" && (
{selectedEngine !== "" && (
<>
<FormControl className={c.planetDropdown} variant="filled">
<InputLabel>Basemap</InputLabel>
Expand All @@ -322,13 +322,13 @@ const NewMissionModal = (props) => {
onChange={(e) => setBasemapProvider(e.target.value)}
label="Basemap"
>
<MenuItem value="none">None (transparent background)</MenuItem>
<MenuItem value="none">None (no basemap)</MenuItem>
<MenuItem value="maplibre">MapLibre GL (open-source)</MenuItem>
<MenuItem value="mapbox">Mapbox GL (requires access token)</MenuItem>
</Select>
</FormControl>
<Typography className={c.subtitle2}>
{`Optional vector-tile basemap rendered beneath deck.gl layers. Can be changed later.`}
{`Optional vector-tile basemap rendered beneath map layers. Can be changed later.`}
</Typography>
{basemapProvider !== "none" && (
<>
Expand Down
2 changes: 1 addition & 1 deletion configure/src/metaconfigs/tab-ui-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
{
"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

"type": "dropdown",
"options": ["none", "maplibre", "mapbox"],
"default": "none",
Expand Down
40 changes: 40 additions & 0 deletions src/essence/Basics/MapEngines/Adapters/DeckGLAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ interface BasemapInstance {
off(type: string, handler: (...args: unknown[]) => void): unknown
/** Recalculate the map size from its container element. */
resize(): void
/** Switch the map to a different style URL at runtime. */
setStyle(styleUrl: string): unknown
}

/**
Expand Down Expand Up @@ -324,6 +326,12 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
return this._basemap
}

setBasemapStyle(styleUrl: string): void {
if (this._basemap) {
this._basemap.setStyle(styleUrl)
}
}

getContainer(): HTMLElement {
return this._container
}
Expand Down Expand Up @@ -823,9 +831,11 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
},
onClick: (info: PickingInfo) => {
this._featureClickHandler?.(pickInfoToResult(info))
this._emitClick(info)
},
onHover: (info: PickingInfo) => {
this._featureHoverHandler?.(pickInfoToResult(info))
this._emitMouseMove(info)
},
} as any)
}
Expand Down Expand Up @@ -890,9 +900,11 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
layers: [],
onClick: (info: PickingInfo) => {
this._featureClickHandler?.(pickInfoToResult(info))
this._emitClick(info)
},
onHover: (info: PickingInfo) => {
this._featureHoverHandler?.(pickInfoToResult(info))
this._emitMouseMove(info)
},
})

Expand Down Expand Up @@ -988,6 +1000,34 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
private _emitEvent(name: string, data?: unknown): void {
this._eventListeners.get(name)?.forEach((h) => h(data as PickingInfo))
}

private _emitClick(info: PickingInfo): void {
if (!info?.coordinate) return
this._eventListeners.get('click')?.forEach(
(h) => h(this._buildNormalizedPointerEvent(info) as unknown as PickingInfo)
)
}

private _emitMouseMove(info: PickingInfo): void {
if (!info?.coordinate) return
this._eventListeners.get('mousemove')?.forEach(
(h) => h(this._buildNormalizedPointerEvent(info) as unknown as PickingInfo)
)
}

private _buildNormalizedPointerEvent(info: PickingInfo): Record<string, unknown> {
const lat = info.coordinate![1]
const lng = info.coordinate![0]
return {
lat,
lng,
latlng: { lat, lng },
containerPoint:
info.x != null && info.y != null
? { x: info.x, y: info.y }
: undefined,
}
}
}

export default DeckGLAdapter
23 changes: 23 additions & 0 deletions src/essence/Basics/MapEngines/Adapters/DeckGLHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,29 @@ function getPropValue(object: unknown, path: string | undefined): unknown {
*
* @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

input: unknown,
fallback: [number, number, number, number]
): [number, number, number, number] {
if (Array.isArray(input) && input.length >= 3) {
const [r, g, b, a] = input as number[]
return [r, g, b, a ?? 255]
}
if (typeof input === 'string') {
let s = input.trim()
if (s.startsWith('#')) s = s.slice(1)
if (s.length === 3) s = s.split('').map((c) => c + c).join('')
if (s.length === 6 || s.length === 8) {
const r = parseInt(s.slice(0, 2), 16)
const g = parseInt(s.slice(2, 4), 16)
const b = parseInt(s.slice(4, 6), 16)
const a = s.length === 8 ? parseInt(s.slice(6, 8), 16) : 255
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) return [r, g, b, a]
}
}
return fallback
}

export function buildDeckLayer(id: string, options: LayerOptions): Layer {
const resolvedType = DECKGL_TYPE_ALIAS[options.type ?? ''] ?? options.type
switch (resolvedType) {
Expand Down
77 changes: 77 additions & 0 deletions src/essence/Basics/MapEngines/Adapters/LeafletAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
FitBoundsOptions,
MapInitOptions,
ProjectionOptions,
BasemapOptions,
} from '../types/view'
import { LayerOptions, TileLayerOptions, MarkerOptions } from '../types/layers'
import { IMapEngineMarkers } from '../IMapEngineMarkers'
Expand All @@ -39,6 +40,7 @@ import {
QueryFeaturesOptions,
} from '../types/events'
import { MapEngineType } from '../types/engine'
import { MAPBOX_STATIC_TILE_OPTIONS, OSM_FALLBACK_TILE } from './LeafletHelpers'

// Leaflet is loaded globally via window.L
declare const L: any
Expand Down Expand Up @@ -79,6 +81,9 @@ export default class LeafletAdapter implements IMapEngine<any, any, any>, IMapEn
*/
private _initOptions: MapInitOptions | null = null

private _basemapLayer: any = null
private _basemapAccessToken: string | undefined

/**
* Initialize the Leaflet map instance
*/
Expand Down Expand Up @@ -154,6 +159,10 @@ export default class LeafletAdapter implements IMapEngine<any, any, any>, IMapEn
if (attributionControl) {
attributionControl.remove()
}

if (options.basemap && options.basemap.provider && options.basemap.provider !== 'none') {
this._initBasemapTileLayer(options.basemap)
}
}

/**
Expand Down Expand Up @@ -247,6 +256,8 @@ export default class LeafletAdapter implements IMapEngine<any, any, any>, IMapEn
destroy(): void {
if (!this._map) return

this._removeBasemapLayer()

this._eventHandlers.forEach((handler, eventName) => {
this._map.off(eventName, handler)
})
Expand All @@ -268,6 +279,10 @@ export default class LeafletAdapter implements IMapEngine<any, any, any>, IMapEn
return this._map
}

getBasemap(): any {
return this._basemapLayer
}

/**
* Get the container element
*/
Expand Down Expand Up @@ -939,4 +954,66 @@ export default class LeafletAdapter implements IMapEngine<any, any, any>, IMapEn
}
return null
}

// ========================================
// 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

this._basemapAccessToken = basemap.accessToken
const spec = this._resolveBasemapTileSpec(basemap)
this._basemapLayer = L.tileLayer(spec.url, spec.options)
this._basemapLayer.addTo(this._map)
this._basemapLayer.bringToBack()

const specMinZoom = (spec.options as { minZoom?: number }).minZoom
if (typeof specMinZoom === 'number' && specMinZoom > this._map.getMinZoom()) {
this._map.setMinZoom(specMinZoom)
}
}

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?

if (!this._map) return
const spec = this._resolveBasemapTileSpec({
style: styleUrl,
accessToken: this._basemapAccessToken,
})
this._removeBasemapLayer()
this._basemapLayer = L.tileLayer(spec.url, spec.options)
this._basemapLayer.addTo(this._map)
this._basemapLayer.bringToBack()
}

private _removeBasemapLayer(): void {
if (this._basemapLayer && this._map) {
this._map.removeLayer(this._basemapLayer)
}
this._basemapLayer = null
}

private _resolveBasemapTileSpec(basemap: Omit<BasemapOptions, 'provider'>): {
url: string
options: Record<string, unknown>
} {
const style = basemap.style || ''

const mapboxMatch = style.match(/^mapbox:\/\/styles\/([^/]+)\/(.+)$/)
if (mapboxMatch) {
const [, user, styleId] = mapboxMatch
const token = basemap.accessToken || this._basemapAccessToken || ''
return {
url: `https://api.mapbox.com/styles/v1/${user}/${styleId}/tiles/{z}/{x}/{y}?access_token=${token}`,
options: { ...MAPBOX_STATIC_TILE_OPTIONS },
}
}

if (style.includes('{z}') && style.includes('{x}') && style.includes('{y}')) {
return { url: style, options: {} }
}

return {
url: OSM_FALLBACK_TILE.url,
options: { ...OSM_FALLBACK_TILE.options },
}
}
}
22 changes: 22 additions & 0 deletions src/essence/Basics/MapEngines/Adapters/LeafletHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ export function buildLeafletMarker(id: string, options: MarkerOptions): any {
return marker
}

/**
* Mapbox Static Tiles API returns 512px tiles. `zoomOffset: -1` corrects
* Leaflet's 256-px-tile zoom math; `minZoom: 1` follows because Leaflet zoom 0
* would request URL zoom -1 (404). These are protocol-level coupling, not
* mission-tunable defaults.
*/
export const MAPBOX_STATIC_TILE_OPTIONS = {
tileSize: 512,
zoomOffset: -1,
minZoom: 1,
attribution: '© Mapbox © OpenStreetMap',
} as const

/** OSM fallback used when no Mapbox style or {z}/{x}/{y} template is provided. */
export const OSM_FALLBACK_TILE = {
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
options: {
subdomains: 'abc',
attribution: '© OpenStreetMap contributors',
},
} as const

// ─── Private helpers ──────────────────────────────────────────────────────────

/**
Expand Down
Loading