Skip to content
Open
33 changes: 32 additions & 1 deletion API/updateTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ const path = require("path");
const logger = require("./logger");
const { isLean } = require("./Backend/Utils/deploymentMode");

// Module binding (a `paths` key) -> the tool's address: the name it answers to
// on the bus, in the modern controller's registries and in teardown events.
// toolCanonicalId (src/essence/Basics/ToolController_/ToolMetadataUtils.js)
// applies the same derivation in the browser, so the build and the frontend
// agree on what a tool is called. Two bindings can derive one address (`Foo`
// and `FooTool`), so a collision throws here and fails the build.
function buildToolIds(tools) {
const ids = {};
const claimedBy = new Map();
for (const t in tools) {
for (const p in tools[t].paths) {
const id = p.replace(/Tool$/, "").toLowerCase();
if (claimedBy.has(id) && claimedBy.get(id) !== p)
throw new Error(
`Tool bindings "${claimedBy.get(id)}" and "${p}" both derive the address "${id}"`
);
claimedBy.set(id, p);
ids[p] = id;
}
}
return ids;
}

function updateTools() {
let tools = {};

Expand Down Expand Up @@ -189,6 +212,9 @@ function updateTools() {
toolConfigs += `export const toolModules = ${JSON.stringify(
toolModules
).replace(/"/g, "")}\n`;
toolConfigs += `export const toolIds = ${JSON.stringify(
buildToolIds(tools)
)}\n`;
toolConfigs += `export const testModules = ${JSON.stringify(
testModules
).replace(/"/g, "")}\n`;
Expand Down Expand Up @@ -383,4 +409,9 @@ function updateComponents() {
}
}

module.exports = { updateTools, updateComponents, bakeStaticConfig };
module.exports = {
updateTools,
updateComponents,
bakeStaticConfig,
buildToolIds,
};
48 changes: 31 additions & 17 deletions docs/pages/APIs/JavaScript/Main/Event-Bus-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,11 @@ anything there right now.

## Plugin Scoped API

MMGIS automatically injects a scoped API into each tool as `this.api`. This API automatically prefixes event and provider names with `plugin:{address}:`, where `address` is derived from the tool's module name (e.g., `DrawTool` → `draw`).
MMGIS injects a scoped API into each tool the modern layout loads as `this.api`. This API automatically prefixes event and provider names with `plugin:{address}:`, where `address` is derived at build time from the tool's module binding (e.g., `DrawTool` → `draw`). A tool never mints its own handle — there is no public way to — so a plugin reaches its own and no other's.

> **Note:** Each plugin must have a unique ID. Multiple instances of the same plugin in a mission are not currently supported. If two plugins share the same ID, their events and providers will collide. This constraint is not currently enforced at runtime but may be in a future version.
The controller mints the handle before the tool's `initialize()` runs and releases it after the tool's `destroy()` returns, unregistering every provider and subscription made through it. Anything a tool registers straight on `window.mmgisAPI` sits outside the handle and stays the tool's own to remove: the React-based tools work that way today, so their requests carry no caller and LayerManager's unprefixed providers outlive a release.

> **Note:** An address comes from a tool's module binding, and two bindings that derive the same address fail the build. One tool still cannot run as two instances: the second would answer for the first's events and providers.

The scoped API is available on `this.api` in your tool's `initialize()` and `make()` functions:

Expand All @@ -179,13 +181,13 @@ const MyTool = {
Emit an event with auto-prefixed name.

```javascript
const api = window.mmgisAPI.forPlugin('myPlugin')
const api = this.api // injected; address 'myplugin'

// This emits 'plugin:myPlugin:dataUpdated'
// This emits 'plugin:myplugin:dataUpdated'
api.emit('dataUpdated', { value: 42 })

// Subscribers listen using the full path
window.mmgisAPI.on('plugin:myPlugin:dataUpdated', (data) => {
window.mmgisAPI.on('plugin:myplugin:dataUpdated', (data) => {
console.log(data.value) // 42
})
```
Expand All @@ -197,7 +199,7 @@ Subscribe to an event. Like `request`, names are **not** prefixed — a subscrip
The handle tracks the subscription, so `release()` drops it along with the plugin's providers. The returned unsubscribe is there for letting one go sooner.

```javascript
const api = window.mmgisAPI.forPlugin('myPlugin')
const api = this.api

const off = api.on('layer:visibilityChange', handleLayerChange)

Expand All @@ -211,15 +213,15 @@ Register a provider with auto-prefixed name.
**Returns:** Cleanup function

```javascript
const api = window.mmgisAPI.forPlugin('myPlugin')
const api = this.api // injected; address 'myplugin'

// This registers 'plugin:myPlugin:getData'
// This registers 'plugin:myplugin:getData'
const cleanup = api.provide('getData', (params) => {
return { result: params.input * 2 }
})

// Callers request using the full path
const data = await window.mmgisAPI.request('plugin:myPlugin:getData', { input: 21 })
const data = await window.mmgisAPI.request('plugin:myplugin:getData', { input: 21 })
console.log(data.result) // 42

// Later, remove the provider
Expand All @@ -231,9 +233,9 @@ cleanup()
Request another provider, stamped with this plugin's address. Names are **not** prefixed: a request addresses someone else's provider, so it takes the full name.

```javascript
const api = window.mmgisAPI.forPlugin('myPlugin')
const api = this.api // injected; address 'myplugin'

// The provider is called with ({ input: 21 }, { caller: 'myPlugin' })
// The provider is called with ({ input: 21 }, { caller: 'myplugin' })
await api.request('plugin:other:getData', { input: 21 })
```

Expand All @@ -248,22 +250,22 @@ Hand every registration this handle made back to core — its own `getVars` prov
After release the handle is inert: `emit`, `provide` and `on` do nothing, `request` resolves to `null`, and releasing again changes nothing.

```javascript
const api = window.mmgisAPI.forPlugin('myPlugin')
api.provide('getData', () => data) // 'plugin:myPlugin:getData'
const api = this.api // injected; address 'myplugin'
api.provide('getData', () => data) // 'plugin:myplugin:getData'

api.release()
window.mmgisAPI.hasHandler('plugin:myPlugin:getData') // false
window.mmgisAPI.hasHandler('plugin:myplugin:getData') // false
```

### Metadata Properties

The scoped API also exposes metadata:

```javascript
const api = window.mmgisAPI.forPlugin('myPlugin')
const api = this.api

console.log(api.address) // 'myPlugin'
console.log(api.prefix) // 'plugin:myPlugin:'
console.log(api.address) // 'myplugin'
console.log(api.prefix) // 'plugin:myplugin:'
```

### Complete Plugin Example
Expand Down Expand Up @@ -386,6 +388,8 @@ window.mmgisAPI.on('legend:made', ({ layerName, legendData }) => {
|-------|---------|-------------|
| `panels:changed` | `{ panels }` | Fired whenever the panel layout changes — a panel registered or unregistered, changed state, lost a tool, or was resized — and once with an empty listing when the layout is torn down |
| `plugins:changed` | `{ plugins }` | Fired whenever a plugin is shown, hidden, loaded or unloaded by command, once after a batch of plugins loads with the layout, and once with an empty listing when the layout is torn down |
| `plugins:destroyed` | `{ pluginId }` | Fired as one plugin is torn down, after its own `destroy()` has run and its bus handle has been released |
| `plugins:allDestroyed` | `{ pluginIds }` | Fired once when a layout teardown destroyed at least one plugin, after each plugin's own `plugins:destroyed` |

`panels` carries the same listing [`panels:getAll`](#panel-and-plugin-providers)
returns, and `plugins` the same listing `plugins:getAll` returns, so there is
Expand Down Expand Up @@ -416,6 +420,16 @@ A component that seeds from `panels:getAll` and also subscribes to
`panels:changed` must guard the seed so it cannot overwrite state an event
has already delivered — the request can resolve after a later event lands.

`plugins:destroyed` and `plugins:allDestroyed` report the teardown itself
rather than the listing that results from it. Both are signals a core service
releases shared resources on. `pluginId` is the departing plugin's address —
the identity it spoke to services under — so a release matched against it
reaches only what that plugin held, and a surviving plugin's stays put. The
collective signal releases outright, because with every plugin destroyed the
resource's owner is among them and no bystander pays for the release. A
teardown a command asked for is followed by `plugins:changed` carrying the new
listing.

### WebSocket Events

| Event | Payload | Description |
Expand Down
18 changes: 6 additions & 12 deletions src/essence/Basics/Layers_/Layers_.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Search from '../../Ancillary/Search'
import Attributions from '../../Ancillary/Attributions'
import CursorInfo from '../../Ancillary/CursorInfo'
import ToolController_ from '../../Basics/ToolController_/ToolController_'
import { toolCanonicalId } from '../ToolController_/ToolMetadataUtils'
import LayerGeologic from './LayerGeologic/LayerGeologic'
import ServiceUrls from '../ServiceUrls/ServiceUrls'
import { isRasterTileLayerType } from '../MapEngines/types/engine'
Expand Down Expand Up @@ -2486,22 +2487,15 @@ const L_ = {
L_.Map_.resetView(L_.configData.msv.view)
L_.Globe_.litho.setCenter(L_.configData.msv.view)
},
hasTool: function (toolName) {
for (var i = 0; i < L_.tools.length; i++) {
if (
L_.tools[i].hasOwnProperty('name') &&
L_.tools[i].name.toLowerCase() == toolName
)
return true
}
return false
},
getToolVars: function (toolName, withVarsFromLayers, showWarnings) {
let vars = {}
for (var i = 0; i < L_.tools.length; i++) {
// Matched on the tool's address first, with the lowercased display
// name kept as a fallback for callers that ask by that instead.
if (
L_.tools[i].hasOwnProperty('name') &&
L_.tools[i].name.toLowerCase() == toolName &&
(toolCanonicalId(L_.tools[i]) === toolName ||
(L_.tools[i].hasOwnProperty('name') &&
L_.tools[i].name.toLowerCase() == toolName)) &&
L_.tools[i].hasOwnProperty('variables')
) {
vars = L_.tools[i].variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ The validated dashboard configuration declares available UI panels (e.g., left,

Tools defined in the mission configuration are processed to generate normalized metadata. The `buildToolConfigMap` function parses properties like layout orientation, preferred positions, and custom icons. This step produces a clean `ToolMetadata` object used for capability matching, separating visual logic from core tool behavior.

Metadata carries two names for a tool. `module` is the binding that reaches its class in the generated registry (`src/pre/tools.js`); `id` is its address — what it is called on the bus, in the controller's registries, in `data-tool` attributes and in teardown events. The address is derived from the binding at build time (`AOITool` → `aoi`), so a panel may name a tool by its display name, its address or its binding and reach the same tool.

## 4. Tool Assignment
**Module:** `src/essence/Basics/ToolController_/ToolControllerModern_.js`

Expand All @@ -42,4 +44,4 @@ The `UserInterfaceModern_.render()` method constructs the physical HTML structur

To prevent DOM race conditions and ensure CSS layout calculations are finalized, the pending `toolLoadQueue` is executed asynchronously using `setTimeout(fn, 0)`.

Once triggered, `ToolControllerModern_.loadTool()` is called for each pending tool. This method locates the tool module, calls its `initialize()` method, and delegates DOM injection by invoking the tool's `make(targetId)` method inside its assigned placeholder container.
Once triggered, `ToolControllerModern_.loadTool()` is called for each pending tool. This method locates the tool module, mints the tool's plugin-scoped bus handle and assigns it to the instance as `api`, calls its `initialize()` method, and delegates DOM injection by invoking the tool's `make(targetId)` method inside its assigned placeholder container. `destroyTool` releases that handle after the tool's own `destroy()` has run, then announces the teardown as `plugins:destroyed` with the tool's address.
Loading