// React import React, { forwardRef, ReactElement, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; // Geovisto import { Geovisto, ILayerTool, IMapTool, IMapToolsManager } from 'geovisto'; import { ISidebarTool, SidebarToolDefaults } from 'geovisto-sidebar'; // Internal imports import { SidebarTool } from './SidebarTool'; import { ENABLED_PROP, ID_PROP } from '../Constants'; import { getToolInstance } from '../Constructor'; import { isLayerTool } from '../Helpers'; import { useDidUpdateEffect } from '../Hooks'; import { supportedComponentTypes } from '../SupportedToolTypes'; import { IReactElement, ISidebarToolHandle, IToolData, IToolGroupHandle, IToolGroupProps, IToolInfo } from '../types'; /** * Component which processes all supported modules */ export const ToolGroup = forwardRef((props, ref) : JSX.Element => { const [manager, setManager] = useState(Geovisto.createMapToolsManager([])); // Array to keep flag for every tool signalizing if the tool has been changed // but is not visible, so it needs to be re-initialized once is visible again const [toolDirtyBitArray, setToolDirtyBitArray] = useState<[string, boolean][]>([]); // Queue for storing tools enable/disable operation ka const [enabledToolQueue, setEnabledToolQueue] = useState<[string, boolean][]>([]); const sidebarRef = useRef(null); const childrenExtended: ReactElement[] = []; let updatingManagerLock = false; const setLock = (value: boolean) => updatingManagerLock = value; // Expose method to process and re-render the tools useImperativeHandle(ref, () => ({ rerenderTools: () => rerender() })); // After the updated manager was set, run the queued enable/disable tool tasks useEffect(() => { setLock(false); if(enabledToolQueue.length !== 0) { enabledToolQueue.forEach(toolData => { const id = toolData[0] as string; const enabled = toolData[1] as boolean; const tool = manager.getById(id) as IMapTool; if(tool !== undefined) { tool.setEnabled(enabled); tool.getProps().enabled = enabled; } }); setEnabledToolQueue([]); } }, [manager]); /** * Sets dirty bit if tool properties was changed while not enabled */ const setDirtyBit = (id: string, bit: boolean) => { const toolRecord: [string, boolean] = [id, bit]; setToolDirtyBitArray(arr => [...arr.map(tool => tool[0] === id ? toolRecord : tool)]); }; /** * Returns dirty bit by tool id */ const getDirtyBit = (id: string): boolean => { const record = toolDirtyBitArray.find(tool => tool[0] === id); return record ? record[1] : false; }; /** * Replaces id in dirty bit array */ const replaceIdInDirtyBitArray = (prevId: string, newId: string): void => { // Get index of the tool const toolDirtyBitArrayCopy = [...toolDirtyBitArray]; const index = toolDirtyBitArray.findIndex(tool => tool[0] === prevId); // Set new Id toolDirtyBitArrayCopy[index][0] = newId; setToolDirtyBitArray(toolDirtyBitArrayCopy); }; /** * Rerenders the content with the Sidebar in mind */ const rerender = () => { if(childrenExtended?.some(el => el.type === SidebarTool)) { sidebarRef.current?.getTabs(); } else { emitRerender(); } } /** * Emits parent callback to re-render the map and sets the current state */ const emitRerender = () => { const map = props.onRenderChange?.(manager); if(map !== undefined) { const mapToolsManager = map.getState().getTools(); if (mapToolsManager !== undefined) { setLock(true); setManager(mapToolsManager); // Reset the dirty bit flags, because all tools were re-rendered setToolDirtyBitArray(arr => arr.map(tool => [tool[0], false])); } } }; /** * Sets tool and its sidebar tab as enabled/disabled */ const toolSetEnabled = (toolData: IToolData) => { const tool = manager.getById(toolData.id) as ILayerTool; const sidebarTool = manager.getByType(SidebarToolDefaults.TYPE)[0]; // Disable also checkbox on the tab when the tool is disabled if(sidebarTool !== undefined) { const tabs = (sidebarTool as ISidebarTool).getTabs(); // Find the sidebar tab corresponding to the tool const tab = tabs.find(tab => tab.getTool().getId() === toolData.id); // Enable or disable the tool tab tab?.setChecked(toolData.enabled); } if(toolData.enabled && getDirtyBit(toolData.id)) { // Reset the dirty bit and proceed the tool to process setDirtyBit(toolData.id, false); handleToolChange(toolData); } else if(updatingManagerLock) { setEnabledToolQueue(queue => [...queue, [toolData.id, toolData.enabled]]); } else { tool.setEnabled(toolData.enabled); tool.getProps().enabled = toolData.enabled; } }; /** * Initializes the tool internal reprezentation from React element */ const initTool = (toolData: IToolData, type: IReactElement) => { const tool = getToolInstance(type, toolData); manager.add(tool); setToolDirtyBitArray(arr => [[toolData.id, false], ...arr]); // Added tool were the latest to process, map is ready to render if(manager.getAll().length === childrenCount) { emitRerender(); } }; /** * Removes old tool from the manager and adds new instance with new properties */ const processToolUpdate = (toolData: IToolData, type: IReactElement, currentTool: IToolInfo, property?: string) => { // Remove current tool from manager manager.removeById(currentTool.id); // Add tool with changed properties const processedTool = getToolInstance(type, toolData); manager.add(processedTool); const sidebarTool = manager.getByType(SidebarToolDefaults.TYPE)[0] as ISidebarTool; // Reasons to re-render: // - the sidebar tool is present in configuration and tool is enabled (visible) // - the 'id' property changed -> therefore sidebar tabs must be re-rendered // - the tool is not layer tool, so 'enabled' property change requires re-render const rerenderTool = toolData.enabled || property === ID_PROP || !currentTool.isLayerTool; if(sidebarTool !== undefined && sidebarTool.getProps().enabled && rerenderTool) { // Process all the sidebar tabs so the tool tab reflects changes in tool properties // Ref call will result in calling 'handleToolChange' method directly from Sidebar component with updated data sidebarRef.current?.getTabs(); } else { // Sidebar component is not present, re-render the tool immediately if tool is enabled if(rerenderTool) { emitRerender(); } else { setDirtyBit(toolData.id, true); } } }; /** * Removes old sidebar from the manager and adds new with new properties */ const processSidebarUpdate = (toolData: IToolData, type: IReactElement, currentTool: Record) => { const tool = currentTool.data as ISidebarTool; // Remove current tool from manager manager.removeById(currentTool.id); // Add tool with changed properties const processedTool = getToolInstance(type, toolData); manager.add(processedTool); // Re-render only when the sidebar tool is supposed to be enabled if(toolData.enabled) { emitRerender(); } // Remove sidebar elements from the leaflet map if sidebar has been disabled (no need for rerender) else if(tool.isEnabled() !== toolData.enabled) { const sidebar = tool.getState().getSidebar()?.remove(); if(sidebar) { tool.getState().setSidebar(sidebar); } } }; /** * Handler for the ontToolChange() callback * Resolves tool manager initialization and tool components updates */ const handleToolChange = (toolData: IToolData, property?: string) => { // Get original react element of the tool const toolElement = childrenExtended?.find(el => el.props.id === toolData.id); if(React.isValidElement(toolElement)) { // Initialization: All tools were not initialized yet if(manager.getAll().length < childrenCount) { initTool(toolData, toolElement.type); return; } // Updating: All tools were loaded, handler was triggered by updating tool's properties else { const toolId = property === ID_PROP ? toolData.prevId : toolData.id; const currentTool = manager.getById(toolId) as IMapTool; // If tool is missing in the manager, it means that is being processed (removed and added new) by different // process and will be re-rendered with the current props, therefore this call can be cancelled // Multiple unnecessary calls are caused by using functions as props values if(currentTool === undefined) { return; } // Updating: Standard way of processing props changes - recreate the tool if(manager.getAll().length === childrenCount) { // Decides whether tool implements ILayerTool or IMapTool interface const toolIsLayerTool = isLayerTool(currentTool); switch (property) { case ID_PROP: replaceIdInDirtyBitArray(toolData.prevId, toolData.id); break; case ENABLED_PROP: if(toolIsLayerTool) { toolSetEnabled(toolData); return; } break; default: break; } // Manage updating of a SidebarTool component props if(toolElement.type === SidebarTool) { processSidebarUpdate(toolData, toolElement.type, { id: toolId, data: currentTool, isLayerTool: toolIsLayerTool }); return; } // Manage updating of all others tool component props else if(supportedComponentTypes.includes(toolElement.type)) { processToolUpdate(toolData, toolElement.type, { id: toolId, data: currentTool, isLayerTool: toolIsLayerTool }, property); return; } else { throw Error('Error while providing update - Tool is not supported.'); } } else { throw Error('Error while processing tools.'); } } } }; /** * Validate and process all children elements and add additional props */ React.Children.forEach(props.children, (child, index) => { // Ignore components that are not supported (= provided by this library) if(!React.isValidElement(child)) { return; } else if (!supportedComponentTypes.includes(child.type)) { console.warn(`Following element is not supported and will be skipped.`); console.warn(child); return; } else if(childrenExtended.some(el => (el as ReactElement).props.id === (child as ReactElement).props.id)) { console.warn(`Component with the id "${(child as ReactElement).props.id}" is already defined and duplicate will be skipped.`); return; } const toolProps = {...child.props}; toolProps.key = index; toolProps.onToolChange = handleToolChange; // Add ref to SidebarTool if(child.type === SidebarTool) { toolProps.ref = sidebarRef; } childrenExtended.push(React.cloneElement(child, toolProps, child.props.children)); }); // Get count of all tools configured by user const childrenCount = useMemo(() : number => React.Children.count(childrenExtended) , [childrenExtended]); // Execute when user changes number of ToolGroup child using the conditional rendering useDidUpdateEffect(() => { const validIds = React.Children.map(childrenExtended, (child) => child.props.id); const managerIds = manager.getIds(); if((managerIds.length != validIds.length)) { const invalidIds = managerIds.filter(id => !validIds.includes(id)); invalidIds.forEach(id => manager.removeById(id)); } rerender(); }, [childrenCount]); // Check on start if ToolGrorup has any valid child elements useEffect(() => { // Tool component are not specified -> render map with basic props only if(!childrenExtended?.some((el : ReactElement) => supportedComponentTypes.includes(el.type))) { rerender(); } }, []); return <>{childrenExtended}; });