Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion mission-profiles/full-demo.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"on": [
"Title",
"LayerManager",
"AddTempLayer",
"AOI",
"Card",
"Chart",
Expand Down Expand Up @@ -210,7 +211,8 @@
"defaultState": "expanded"
},
"panelTools": [
"ShareExport"
"ShareExport",
"AddTempLayer"
],
"id": "float-share"
},
Expand Down
24 changes: 23 additions & 1 deletion mission-profiles/generated/full-demo-mission.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@
"defaultState": "expanded"
},
"panelTools": [
"ShareExport"
"ShareExport",
"AddTempLayer"
],
"id": "float-share"
},
Expand Down Expand Up @@ -316,6 +317,27 @@
"justification": "left"
}
},
{
"name": "AddTempLayer",
"icon": "add",
"js": "AddTempLayerTool",
"on": true,
"variables": {},
"metadata": {
"icon": "add",
"compatiblePositions": [
"float-top-left",
"float-top-center",
"float-top-right",
"float-bottom-left",
"float-bottom-center",
"float-bottom-right"
],
"preferredPosition": "float-top-right",
"startHidden": true,
"modernLayoutSupport": true
}
},
{
"name": "Chart",
"icon": "chart-bar",
Expand Down
3 changes: 3 additions & 0 deletions src/essence/Tools/AddTempLayer/config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"defaults": {
"variables": {}
},
"defaultIcon": "add",
"description": "Add an external layer (WMS/WMTS/XYZ/GeoJSON) to the map for the current session.",
"descriptionFull": {
Expand Down
4 changes: 4 additions & 0 deletions src/essence/Tools/LayerManager/MMGISLayerManagerAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
setColormap,
setRescale,
zoomToLayer,
compareLayer,
showAddLayer,
} from './adapters/handlers'
import { mmgisGetLayerBounds } from '../_shared/adapters/mmgisAPI'

Expand Down Expand Up @@ -76,6 +78,8 @@ export function MMGISLayerManagerAdapter() {
onRescaleChange={(id, mn, mx) => { report('setRescale', setRescale(id, mn, mx, refresh)) }}
onZoomToLayer={(id) => { report('zoomToLayer', zoomToLayer(id)) }}
canZoomToLayer={canZoomToLayer}
onCompareLayer={compareLayer}
onAddLayer={showAddLayer}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ const titlesIn = (container: HTMLElement) =>
const rampButtonsIn = (container: HTMLElement) =>
container.querySelectorAll('[title="Change color ramp"]')

/** Open a row's kebab and read its items out of the portal they render into. */
const openMenu = async (container: HTMLElement) => {
await click(container.querySelector('[title="More options"]')!)
return Array.from(document.body.querySelectorAll('[role="menuitem"]'))
}

const menuItem = (items: Element[], label: string) =>
items.find((el) => el.textContent?.includes(label))

beforeEach(() => {
// Deleted rather than stubbed, so calling through either one throws.
delete (window as { mmgisAPI?: unknown }).mmgisAPI
Expand Down Expand Up @@ -103,6 +112,28 @@ describe('LayerManagerPanel without a host', () => {
await unmount()
})

test('omits the add-layer button unless the host handles it', async () => {
const { container, unmount } = await mount(
<LayerManagerPanel layers={[GRADIENT_LAYER]} />,
)
expect(container.querySelector('.blocks-layer-manager__add-layer')).toBeNull()
await unmount()
})

test('reports add-layer clicks through its callback', async () => {
const onAddLayer = vi.fn()
const { container, unmount } = await mount(
<LayerManagerPanel layers={[GRADIENT_LAYER]} onAddLayer={onAddLayer} />,
)

const button = container.querySelector('.blocks-layer-manager__add-layer')!
expect(button.textContent).toContain('Add layer from URL')
await click(button)

expect(onAddLayer).toHaveBeenCalledTimes(1)
await unmount()
})

test('reports visibility changes through its callback', async () => {
const onVisibilityChange = vi.fn()
const { container, unmount } = await mount(
Expand Down Expand Up @@ -154,6 +185,41 @@ describe('LayerManagerPanel without a host', () => {
await unmount()
})

test('offers the comparison hand-off only when the host wires one', async () => {
const onCompareLayer = vi.fn()
const wired = await mount(
<LayerManagerPanel
layers={[GRADIENT_LAYER]}
onCompareLayer={onCompareLayer}
/>,
)
await click(menuItem(await openMenu(wired.container), 'Compare layer')!)
expect(onCompareLayer).toHaveBeenCalledWith(GRADIENT_LAYER.id)
await wired.unmount()

const unwired = await mount(<LayerManagerPanel layers={[GRADIENT_LAYER]} />)
expect(menuItem(await openMenu(unwired.container), 'Compare layer'))
.toBeUndefined()
await unwired.unmount()
})

test('holds the comparison hand-off shut for a layer that is switched off', async () => {
const onCompareLayer = vi.fn()
const { container, unmount } = await mount(
<LayerManagerPanel
layers={[{ ...GRADIENT_LAYER, visible: false }]}
onCompareLayer={onCompareLayer}
/>,
)

const item = menuItem(await openMenu(container), 'Compare layer')!
expect(item.getAttribute('aria-disabled')).toBe('true')
expect(item.getAttribute('title')).toBe('Turn this layer on to compare it')
await click(item)
expect(onCompareLayer).not.toHaveBeenCalled()
await unmount()
})

test('renders without any callbacks wired', async () => {
const { container, unmount } = await mount(
<LayerManagerPanel layers={[GRADIENT_LAYER]} />,
Expand Down
41 changes: 41 additions & 0 deletions src/essence/Tools/LayerManager/__tests__/handlers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
setColormap,
setRescale,
zoomToLayer,
compareLayer,
showAddLayer,
} from '../adapters/handlers.ts'
import {
ZOOM_TO_LAYER_PADDING,
Expand All @@ -29,6 +31,9 @@ const setupMock = (responses = {}, emitCalls = []) => {
return { emitCalls, requests }
}

// showAddLayer fires its request without returning it.
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))

const EDITABLE = { hasColormap: true, canChangeColormap: true }
// What an image layer reports: a ramp to show, but nothing to change.
const READ_ONLY = { hasColormap: true, canChangeColormap: false }
Expand Down Expand Up @@ -260,4 +265,40 @@ test.describe('handlers', () => {
expect(requests).toHaveLength(0)
warn.mockRestore()
})

test('compareLayer announces the layer on the bus', () => {
const { emitCalls, requests } = setupMock()
compareLayer('layerA')

expect(emitCalls).toEqual([
{
event: 'plugin:comparison:startWithLayer',
payload: { layerId: 'layerA' },
},
])
expect(requests).toHaveLength(0)
})

test('showAddLayer commands the layout to reveal the form', async () => {
const { emitCalls, requests } = setupMock({
'plugins:show': { ok: true, state: 'visible', changed: true },
})
showAddLayer()
await flush()

expect(requests).toEqual([
{ name: 'plugins:show', params: { pluginId: 'AddTempLayerTool' } },
])
expect(emitCalls).toHaveLength(0)
})

test('showAddLayer logs a refusal instead of dropping it', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
setupMock({ 'plugins:show': { ok: false, reason: 'not-found' } })
showAddLayer()
await flush()

expect(warn).toHaveBeenCalledWith(expect.stringContaining('not-found'))
warn.mockRestore()
})
})
28 changes: 28 additions & 0 deletions src/essence/Tools/LayerManager/adapters/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
mmgisRequest,
mmgisEmit,
mmgisShowPlugin,
mmgisGetLayerCogCapabilities,
mmgisGetLayerBounds,
mmgisFitBounds,
Expand Down Expand Up @@ -65,6 +66,33 @@ export const zoomToLayer = async (layerId: string): Promise<void> => {
})
}

/**
* Hands a layer to the Comparison plugin as the first of the two sides it
* swipes between. A mission without that plugin has nobody listening.
*/
export const compareLayer = (layerId: string): void => {
mmgisEmit('plugin:comparison:startWithLayer', { layerId })
}

export const ADD_LAYER_PLUGIN_ID = 'AddTempLayerTool'

/**
* Reveals the "add layer from URL" form, loading the tool first if the mission
* starts it unloaded. Its own `addTempLayer:show` event would not reach it
* there, being listened for only while the tool is mounted.
*/
export const showAddLayer = (): void => {
// Widened from CommandResult: without strictNullChecks a boolean
// discriminant does not narrow, so `reason` is unreachable on the union.
mmgisShowPlugin(ADD_LAYER_PLUGIN_ID)
.then((result: { ok: boolean; reason?: string }) => {
if (!result.ok) {
console.warn(`LayerManager: showAddLayer refused: ${result.reason}`)
}
})
.catch((err) => console.warn('LayerManager: showAddLayer failed', err))
}

export const setColormap = async (layerId: string, colormap: string, refresh: Refresh): Promise<void> => {
if (!(await canChangeColormap(layerId))) return
await mmgisRequest('layers:updateConfig', { layerUUID: layerId, updates: { currentCogColormap: colormap } })
Expand Down
19 changes: 19 additions & 0 deletions src/essence/Tools/LayerManager/lib/geo/LayerLegend/LayerLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type LayerLegendProps = {
onRescaleChange?: (layerId: string, min: number, max: number) => void
onZoomToLayer?: (layerId: string) => void
canZoomToLayer?: (layerId: string) => Promise<boolean>
onCompareLayer?: (layerId: string) => void
}

export function LayerLegend({
Expand All @@ -56,6 +57,7 @@ export function LayerLegend({
onRescaleChange,
onZoomToLayer,
canZoomToLayer,
onCompareLayer,
}: LayerLegendProps) {
const {
id,
Expand Down Expand Up @@ -162,6 +164,23 @@ export function LayerLegend({
: undefined,
onSelect: () => onZoomToLayer?.(id),
},
// Inert rather than absent on a layer that is switched off: a
// comparison reads two drawn layers against each other, and the way to
// reach it should stay visible meanwhile.
...(onCompareLayer
? [
{
id: 'compare-layer',
label: 'Compare layer',
icon: 'compare-layer',
disabled: !isVisible,
title: !isVisible
? 'Turn this layer on to compare it'
: undefined,
onSelect: () => onCompareLayer(id),
} satisfies PopoverMenuItem,
]
: []),
]

const handleVisibilityToggle = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type LayerLegendListProps = {
onRescaleChange?: LayerLegendProps['onRescaleChange']
onZoomToLayer?: LayerLegendProps['onZoomToLayer']
canZoomToLayer?: LayerLegendProps['canZoomToLayer']
onCompareLayer?: LayerLegendProps['onCompareLayer']
}

export function LayerLegendList({
Expand All @@ -24,6 +25,7 @@ export function LayerLegendList({
onRescaleChange,
onZoomToLayer,
canZoomToLayer,
onCompareLayer,
}: LayerLegendListProps) {
if (!layers || layers.length === 0) {
return (
Expand All @@ -45,6 +47,7 @@ export function LayerLegendList({
onRescaleChange={onRescaleChange}
onZoomToLayer={onZoomToLayer}
canZoomToLayer={canZoomToLayer}
onCompareLayer={onCompareLayer}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export type LayerManagerPanelProps = {
onRescaleChange?: (layerId: string, min: number, max: number) => void
onZoomToLayer?: LayerLegendListProps['onZoomToLayer']
canZoomToLayer?: LayerLegendListProps['canZoomToLayer']
onCompareLayer?: LayerLegendListProps['onCompareLayer']
/** Opens the host's "add layer" surface. No handler, no button. */
onAddLayer?: () => void
}

export function LayerManagerPanel({
Expand All @@ -30,9 +33,26 @@ export function LayerManagerPanel({
onRescaleChange,
onZoomToLayer,
canZoomToLayer,
onCompareLayer,
onAddLayer,
}: LayerManagerPanelProps) {
return (
<div className="blocks-layer-manager">
{onAddLayer && (
<div className="blocks-layer-manager__header">
<button
type="button"
className="blocks-layer-manager__add-layer"
onClick={onAddLayer}
>
<span
className="blocks-layer-manager__add-layer-icon"
aria-hidden="true"
/>
<span>Add layer from URL</span>
</button>
</div>
)}
<div className="blocks-layer-manager__content">
{loading ? (
<div className="blocks-layer-manager__loading">
Expand All @@ -49,6 +69,7 @@ export function LayerManagerPanel({
onRescaleChange={onRescaleChange}
onZoomToLayer={onZoomToLayer}
canZoomToLayer={canZoomToLayer}
onCompareLayer={onCompareLayer}
/>
)}
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/essence/Tools/LayerManager/lib/geo/icons/add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading