Skip to content
Open
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
24 changes: 17 additions & 7 deletions src/essence/Basics/Layers_/Layers_.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ const L_ = {
const uuid = L_.asLayerUUID(layerUUID)
return L_.layers.on?.[uuid] === true
}),
// In-memory layer add/remove (not persisted; lost on reload).
// layerObj requires { name, type, ... }. See mmgisAPI.addLayer.
window.mmgisAPI.provide('layers:addLayer', (layerObj) =>
window.mmgisAPI.addLayer(layerObj)
),
window.mmgisAPI.provide('layers:removeLayer', (layerUUID) =>
window.mmgisAPI.removeLayer(layerUUID)
),
window.mmgisAPI.provide('tool:getVars', (toolName) => L_.getToolVars(toolName)),
window.mmgisAPI.provide('app:isMobile', () => L_.UserInterface_?.isMobile === true),
window.mmgisAPI.provide('app:getMissionPath', () => L_.missionPath),
Expand Down Expand Up @@ -416,7 +424,11 @@ const L_ = {
nextUrl = `/${nextUrl}`
}
}
if (process.env.NODE_ENV === 'development' && F_.isUrlAbsolute(nextUrl)) {
if (
process.env.NODE_ENV === 'development' &&
process.env.ENABLE_CORS_PROXY === 'true' &&
F_.isUrlAbsolute(nextUrl)
) {
try {
if (new URL(nextUrl).origin !== window.location.origin) {
const rootPath = window?.mmgisglobal?.ROOT_PATH || ''
Expand Down Expand Up @@ -3351,12 +3363,10 @@ const L_ = {
await L_.removeLayerFromLayersData(layerName)
}

if (ToolController_.activeToolName === 'LayersTool') {
const layersTool = ToolController_.getTool('LayersTool')
if (layersTool.destroy && layersTool.make) {
layersTool.destroy()
layersTool.make()
}
// Notify subscribers (e.g. the Layers panel) that the layer list
// changed, so they rebuild. Works in classic and modern layouts.
if (window.mmgisAPI) {
window.mmgisAPI.emit('layers:listChanged')
}
},
addLayerToLayersData: async function (layerName) {
Expand Down
43 changes: 40 additions & 3 deletions src/essence/Basics/MapEngines/Adapters/DeckGLHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type TransitionInterpolator,
} from '@deck.gl/core'
import { GeoJsonLayer, BitmapLayer, PointCloudLayer, ScatterplotLayer } from '@deck.gl/layers'
import { TileLayer, Tile3DLayer, MVTLayer } from '@deck.gl/geo-layers'
import { TileLayer, Tile3DLayer, MVTLayer, _WMSLayer as WMSLayer } from '@deck.gl/geo-layers'
import { Tiles3DLoader } from '@loaders.gl/3d-tiles'
import { color as parseColor } from 'd3'

Expand Down Expand Up @@ -182,10 +182,30 @@ function getPropValue(object: unknown, path: string | undefined): unknown {
)
}

/**
* Split a full WMS url into its service endpoint and LAYERS list. Mirrors the
* param parsing Leaflet's WMSColorFilter does, so a single layer url renders
* the same way in both engines.
*/
function parseWmsUrl(url: string): { base: string; layers: string[] } {
const qIdx = url.indexOf('?')
const base = qIdx === -1 ? url : url.slice(0, qIdx)
const search = qIdx === -1 ? '' : url.slice(qIdx + 1)
let layersVal = ''
for (const [key, val] of new URLSearchParams(search)) {
if (key.toLowerCase() === 'layers') {
layersVal = val
break
}
}
const layers = layersVal ? layersVal.split(',').filter(Boolean) : []
return { base, layers }
}

/**
* Construct a deck.gl layer from a {@link LayerOptions} spec.
* Supports `'tile'` (TileLayer + BitmapLayer), `'vector'` (GeoJsonLayer),
* and `'pointcloud'` (PointCloudLayer).
* Supports `'tile'` (TileLayer + BitmapLayer, or WMSLayer when tileformat is
* 'wms'), `'vector'` (GeoJsonLayer), and `'pointcloud'` (PointCloudLayer).
* DeckGL class names (e.g. `'GeoJsonLayer'`) are automatically normalised
* via {@link DECKGL_TYPE_ALIAS}. Use `nativeOptions` for deck.gl-specific props.
*
Expand All @@ -197,6 +217,23 @@ export function buildDeckLayer(id: string, options: LayerOptions): Layer {
case 'tile': {
const o = options as TileLayerOptions
const tileElevation = Number(o.tileElevation)

// WMS can't be a {z}/{x}/{y} template — the GetMap BBOX is computed
// per view. Route to deck.gl's WMSLayer, parsing the service URL and
// LAYERS out of the full WMS url (same params Leaflet reads).
if (o.tileformat === 'wms') {
const { base, layers } = parseWmsUrl(o.url)
return new WMSLayer({
id,
data: base,
serviceType: 'wms',
layers,
srs: 'EPSG:3857',
opacity: o.opacity ?? 1,
...(o.nativeOptions ?? {}),
}) as unknown as Layer
}

return new TileLayer({
id,
data: o.url,
Expand Down
2 changes: 2 additions & 0 deletions src/essence/Basics/MapEngines/types/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface TileLayerOptions extends LayerOptions {
maxNativeZoom?: number
tileSize?: number
tileElevation?: number
/** 'wms' => deck.gl WMSLayer; else a {z}/{x}/{y} url template. */
tileformat?: string
nativeOptions?: Record<string, unknown>
}

Expand Down
1 change: 1 addition & 0 deletions src/essence/Basics/Map_/Map_.js
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,7 @@ async function makeTileLayer(layerObj, mapContext = null) {
ctx.layerRegistry.layer[layerObj.name] = buildDeckLayer(layerObj.name, {
type: layerObj.type || 'tile',
url: layerUrl,
tileformat: tileFormat,
opacity: ctx.layerRegistry.opacity[layerObj.name] || 1,
minZoom: parseInt(layerObj.minZoom),
maxNativeZoom: parseInt(layerObj.maxNativeZoom),
Expand Down
45 changes: 45 additions & 0 deletions src/essence/Tools/AddTempLayer/AddTempLayerTool.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* AddTempLayerTool — MMGIS tool wrapper for the AddTempLayer plugin.
*
* Mounts the React tree into the panel target MMGIS assigns via make(targetId).
* All bus wiring lives in MMGISAddTempLayerAdapter; the lib/ tree is portable.
*/
import React from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { MMGISAddTempLayerAdapter } from './MMGISAddTempLayerAdapter'

let _root: Root | null = null

const AddTempLayerTool = {
height: 0,
width: 300 as number | 'full',
targetId: null as string | null,
made: false,

make(targetId?: string) {
this.targetId = typeof targetId === 'string' ? targetId : 'toolPanel'
const container = document.getElementById(this.targetId)
if (!container) {
console.error(`AddTempLayerTool: container ${this.targetId} not found`)
return
}
_root = createRoot(container)
_root.render(<MMGISAddTempLayerAdapter />)
this.made = true
},

destroy() {
if (_root) {
_root.unmount()
_root = null
}
this.targetId = null
this.made = false
},

getUrlString() {
return ''
},
}

export default AddTempLayerTool
22 changes: 22 additions & 0 deletions src/essence/Tools/AddTempLayer/MMGISAddTempLayerAdapter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useCallback } from 'react'
import { AddTempLayerPanel } from './lib'
import type { AddTempLayerInput } from './lib'
import { buildLayerObj } from './adapters/buildLayerObj'
import { mmgisRequest } from '../_shared/adapters/mmgisAPI'

/**
* Bridges the portable AddTempLayerPanel to MMGIS via the event bus. The panel
* owns its own form state; this adapter only turns a submitted input into a
* `layers:addLayer` request (session-only add; lost on reload).
*/
export function MMGISAddTempLayerAdapter() {
const onAddLayer = useCallback((input: AddTempLayerInput) => {
const layerObj = buildLayerObj(input)
if (!layerObj) {
return Promise.reject(new Error('Invalid or unrecognized layer URL'))
}
return mmgisRequest('layers:addLayer', layerObj)
}, [])

return <AddTempLayerPanel onAddLayer={onAddLayer} />
}
76 changes: 76 additions & 0 deletions src/essence/Tools/AddTempLayer/adapters/buildLayerObj.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// MMGIS-coupled transform: AddTempLayerInput → an MMGIS layerObj for the
// generic `layers:addLayer` channel. In-memory only (not persisted), so the
// layer shows in the Layers panel but is lost on reload.
import type { AddTempLayerInput } from '../lib/types'
import { validateUrl, detectLayerType } from '../lib/utils/url'

// Monotonic suffix so two adds in the same millisecond still get unique names.
let _tempLayerCounter = 0

/** Unique, colon-free layer name. addLayer rejects duplicates; colons break keying. */
export function uniqueLayerName(): string {
return `temp-layer-${Date.now()}-${_tempLayerCounter++}`
}

/**
* Build an MMGIS layerObj. Returns null if the URL is invalid or the type
* can't be detected.
*
* The URL is passed through VERBATIM — we never construct, rewrite, or
* substitute anything in it. We assume the user pasted a URL already in a
* supported, ready-to-use form (the modal documents those forms); if it isn't,
* it simply won't render, which is the user's responsibility, not ours.
*
* Type → MMGIS mapping:
* geojson → { type: 'vector', url }
* wms → { type: 'tile', tileformat: 'wms', url } — the engine appends
* BBOX/WIDTH/HEIGHT per tile and parses LAYERS/FORMAT from the url.
* wmts / xyz → { type: 'tile', tileformat: 'wmts', url } — a {z}/{x}/{y}
* template the engine fills per tile; must already be one.
*/
export function buildLayerObj(
input: AddTempLayerInput,
): ({ name: string } & Record<string, unknown>) | null {
const url = (input.url || '').trim()
if (!validateUrl(url)) return null

const type = input.type || detectLayerType(url)
if (!type) return null

const base = {
name: uniqueLayerName(),
display_name: (input.displayName || '').trim() || 'Imported layer',
// visibility:true so the layer renders as soon as it's added.
visibility: true,
initialOpacity: 1,
}

if (type === 'geojson') {
return { ...base, type: 'vector', url }
}

if (type === 'wms') {
// WMS renders at any zoom (no fixed native tile grid), so no maxNativeZoom.
return {
...base,
type: 'tile',
tileformat: 'wms',
url,
minZoom: 0,
maxZoom: 22,
}
}

// wmts / xyz — a {z}/{x}/{y} tile template the engine fills per tile,
// passed through unchanged.
return {
...base,
type: 'tile',
tileformat: 'wmts',
url,
minZoom: 0,
maxNativeZoom: 18,
maxZoom: 22,
}

}
23 changes: 23 additions & 0 deletions src/essence/Tools/AddTempLayer/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"defaultIcon": "add",
"description": "Add an external layer (WMS/WMTS/XYZ/GeoJSON) to the map for the current session.",
"descriptionFull": {
"title": "Opens a modal to add a temporary external layer from a URL. The layer is session-only and is lost on reload."
},
"hasVars": false,
"name": "AddTempLayer",
"toolbarPriority": 6,
"separatedTool": true,
"paths": {
"AddTempLayerTool": "essence/Tools/AddTempLayer/AddTempLayerTool"
},
"metadata": {
"icon": "add",
"requiredOrientation": "vertical",
"compatiblePositions": ["left", "right"],
"preferredPosition": "left",
"modernLayoutSupport": true,
"width": 300,
"height": 0
}
}
Loading
Loading