terriajs
Version:
Geospatial data visualization platform.
252 lines (224 loc) • 6.95 kB
text/typescript
import { action, computed, makeObservable } from "mobx";
import { ComponentType } from "react";
import createGuid from "terriajs-cesium/Source/Core/createGuid";
import TerriaError from "../../Core/TerriaError";
import ViewerMode from "../../Models/ViewerMode";
import ViewState from "../../ReactViewModels/ViewState";
import { IconGlyph } from "../../Styled/Icon";
import MapNavigationItemController from "./MapNavigationItemController";
import { NavigationItemLocation } from "./MapNavigationModel";
export interface ToolConfig {
/**
* Optional `id` for the tool. If no `id` is passed we generate a random
* one and return it as the result of the `addTool()` call.
*
* `id` is required for other operations on the tool like opening, closing or
* removing it from the toolbar.
*/
id?: string;
/**
* A human readable name for the tool.
*/
name: string;
/**
* A loader function returning the react component module to mount when user activates the tool.
*
* example: ```
* {
* toolComponentLoader: () => import("./MyToolModule.tsx"),
* }
* ```
*/
toolComponentLoader: () => Promise<{ default: ComponentType<any> }>;
/**
* The tool button configuration
*/
toolButton: ToolButton;
/**
* Shows the tool button only for the specified map viewer mode. Defaults to
* showing the tool button in both Cesium (3D) and Leaflet (2D) modes.
*
* eg: Setting this to `ViewerMode.Cesium` will result in the tool button
* being shown only when the map is in Cesium 3D mode.
*/
viewerMode?: ViewerMode;
}
export interface ToolButton {
/**
* Tool button text
*/
text: string;
/**
* Tool button icon
*/
icon: IconGlyph;
/**
* Tooltip to show when hovering over the tool button
*/
tooltip?: string;
/**
* The toolBarSection to place the tool button. Defaults to `TOP`.
*
* The toolbar has 2 sections - TOP and BOTTOM
* TOP - contains the compass and other buttons
* BOTTOM - contains the feedback button
*/
section?: ToolbarSection;
/**
* A number used to determine the order of the button in the toolbar
*/
order?: number;
}
export type ToolbarSection = NavigationItemLocation;
export { default as ViewerMode } from "../../Models/ViewerMode";
/**
* Adds a new tool to the map toolbar
*
* @param viewState The {@link ViewState} instance
* @param config The tool configuration
* @returns `id` of the tool. This will be the same as `config.id` if it was set. Otherwise, a random `id` is generated and returned.
*
* Example:
* const toolId = addTool(viewState, {
* name: "X Tool",
* toolComponentLoader: () => import("./XTool.tsx"),
* toolButton: {
* text: "Open X tool",
* tooltip: "X Tool",
* icon: X_ICON
* }
* })
*/
export function addTool(viewState: ViewState, config: ToolConfig): string {
const id = config.id ?? createGuid();
const controller = new ToolController(viewState, id, config);
const terria = viewState.terria;
terria.mapNavigationModel.addItem({
id,
name: config.toolButton.text,
title: config.toolButton.tooltip,
location: config.toolButton.section ?? "TOP",
order: config.toolButton.order,
controller
});
return id;
}
/**
* Function to programatically open the tool with given ID.
*
* Note that in normal operation, a tool will be opened when user clicks the
* tool button in the map toolbar. This function is useful in situations where
* the tool needs to be opened through other means - like clicking a workbench
* viewing-controls button.
*
* If no tool exists with the given `toolId` then this function does nothing
* and returns `false`. The caller can also check if the call was successful
* by checking the result of {@link isToolOpen()}.
*
* @param viewState The `ViewState` instance.
* @param toolId ID of the tool to open. See {@link addTool}.
* @param props Optional props to pass to the Tool component when activating it.
* @returns `true` if the tool was successfully opened.
*/
export function openTool(viewState: ViewState, toolId: string, props?: any) {
const controller = findToolController(viewState, toolId);
controller?.openTool(props);
return controller?.isToolOpen === true;
}
/**
* Closes the tool with given id.
*
* @param viewState The `ViewState` instance.
* @param toolId ID of the tool to close.
*/
export function closeTool(viewState: ViewState, toolId: string) {
findToolController(viewState, toolId)?.closeTool();
}
/**
* Check if tool with given `id` is currently open.
*
* @param viewState The `ViewState` instance.
* @param toolId ID of the tool to check.
* @returns `true` if the tool is currently open, otherwise returns `false`.
*/
export function isToolOpen(viewState: ViewState, toolId: string): boolean {
return findToolController(viewState, toolId)?.active === true;
}
/**
* Removes the tool with the given id from the toolbar.
*
* @param viewState The `ViewState` instance.
* @param toolId ID of the tool to close.
*/
export function removeTool(viewState: ViewState, toolId: string) {
viewState.terria.mapNavigationModel.remove(toolId);
}
/**
* Find `ToolController` by id.
*/
function findToolController(
viewState: ViewState,
toolId: string
): ToolController | undefined {
const navItem = viewState.terria.mapNavigationModel.items.find(
(it) => it.id === toolId
);
return navItem?.controller instanceof ToolController
? navItem.controller
: undefined;
}
export class ToolController extends MapNavigationItemController {
constructor(
readonly viewState: ViewState,
readonly toolId: string,
readonly toolConfig: ToolConfig
) {
super();
makeObservable(this);
}
get glyph(): { id: string } {
return this.toolConfig.toolButton.icon;
}
get viewerMode(): ViewerMode | undefined {
return this.toolConfig.viewerMode;
}
get isToolOpen() {
return this.viewState.currentTool?.toolName === this.toolConfig.name;
}
get active(): boolean {
return super.active && this.isToolOpen;
}
openTool(props: any = {}) {
const toolConfig = this.toolConfig;
const toolId = this.toolId;
try {
this.viewState.openTool({
toolName: toolConfig.name,
getToolComponent: () =>
toolConfig.toolComponentLoader().then((m) => m.default),
params: {
...props,
// Pass toolId as an extra prop to the component.
// TODO: Maybe we should use react contexts to do this instead of a magic prop?
toolId
}
});
} catch (err) {
this.viewState.terria.raiseErrorToUser(TerriaError.from(err));
}
super.activate();
}
activate() {
this.openTool();
}
deactivate() {
this.closeTool();
}
closeTool() {
if (this.isToolOpen) this.viewState.closeTool();
super.deactivate();
}
}