+
+
+
{'Please note that the layers have resolution restrictions, please ' +
+ ' zoom in and out to see how the trees react to this.'}
+
+
{'Autoconfigured with topmost LayerGroup of passed map:'}
+
+
-
{'Please note that the layers have resolution restrictions, please ' +
- ' zoom in and out to see how the trees react to this.'}
-
- {'Autoconfigured with topmost LayerGroup of passed map:'}
-
-
-
-
+
-
-
{'A LayerTree configured with concrete layerGroup:'}
+
+ {'A LayerTree configured with concrete layerGroup:'}
-
-
+
+
-
-
{'A LayerTree with a filterFunction (The OSM layer is filtered out):'}
+
+ {'A LayerTree with a filterFunction (The OSM layer is filtered out):'}
- layer.get('name') !== 'OSM'}
- />
-
+
layer.get('name') !== 'OSM'}
+ className="bottom"
+ toggleOnClick={true}
+ />
- );
- }
+
+ );
}
diff --git a/src/LayerTree/LayerTree.less b/src/LayerTree/LayerTree.less
new file mode 100644
index 0000000000..3f5232748b
--- /dev/null
+++ b/src/LayerTree/LayerTree.less
@@ -0,0 +1,9 @@
+.react-geo-layertree {
+ .out-of-range {
+ opacity: 0.5;
+ }
+
+ .ant-tree-node-content-wrapper {
+ pointer-events: none;
+ }
+}
diff --git a/src/LayerTree/LayerTree.spec.tsx b/src/LayerTree/LayerTree.spec.tsx
index ee6d4c8400..94007076bd 100644
--- a/src/LayerTree/LayerTree.spec.tsx
+++ b/src/LayerTree/LayerTree.spec.tsx
@@ -1,17 +1,15 @@
-import { getUid } from 'ol';
-import OlCollection from 'ol/Collection';
+import { act,fireEvent, render, screen } from '@testing-library/react';
import OlLayerBase from 'ol/layer/Base';
import OlLayerGroup from 'ol/layer/Group';
import OlLayerTile from 'ol/layer/Tile';
import OlMap from 'ol/Map';
-import * as OlObservable from 'ol/Observable';
import OlSourceTileWMS from 'ol/source/TileWMS';
import * as React from 'react';
-import {
- act
-} from 'react-dom/test-utils';
-import TestUtil from '../Util/TestUtil';
+import TestUtil from '../Util/TestUtil';;
+import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils';
+import userEvent from '@testing-library/user-event';
+
import LayerTree from './LayerTree';
describe('
', () => {
@@ -28,6 +26,8 @@ describe('
', () => {
properties: {
name: 'layer1'
},
+ minResolution: 10,
+ maxResolution: 1000,
source: layerSource1
});
const layerSource2 = new OlSourceTileWMS();
@@ -67,605 +67,468 @@ describe('
', () => {
TestUtil.removeMap(map);
});
- it('is defined', () => {
- expect(LayerTree).not.toBeUndefined();
- });
-
it('can be rendered', () => {
- const wrapper = TestUtil.mountComponent(LayerTree, {map});
- expect(wrapper).not.toBeUndefined();
- });
+ const {
+ container
+ } = render(
);
- it('layergroup taken from map if not provided', () => {
- const wrapper = TestUtil.mountComponent(LayerTree, {map});
- expect(wrapper.state('layerGroup')).toBe(map.getLayerGroup());
+ expect(container).toBeVisible();
});
- it('unmount removes listeners', () => {
- // @ts-ignore
- OlObservable.unByKey = jest.fn();
- const wrapper = TestUtil.mountComponent(LayerTree, {map});
- const olListenerKeys = (wrapper.instance() as LayerTree).olListenerKeys;
- wrapper.unmount();
- expect(OlObservable.unByKey).toHaveBeenCalled();
- expect(OlObservable.unByKey).toHaveBeenCalledWith(olListenerKeys);
- });
+ it('sets the layer name as title per default', () => {
+ renderInMapContext(map,
+
+ );
- it('CWR with new layerGroup rebuild listeners and treenodes ', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
+ const expectedNodeTitles = [
+ 'layer1',
+ 'layer2',
+ 'layerSubGroup'
+ ];
- const subLayer = new OlLayerTile({
- properties: {
- name: 'subLayer'
- },
- source: new OlSourceTileWMS()
- });
- const nestedLayerGroup = new OlLayerGroup({
- properties: {
- name: 'nestedLayerGroup'
- },
- layers: [subLayer]
+ expectedNodeTitles.forEach(expectedNodeTitle => {
+ const node = screen.queryByText(expectedNodeTitle);
+ expect(node).toBeInTheDocument();
});
- expect((wrapper.instance() as LayerTree).olListenerKeys).toHaveLength(10);
- wrapper.setProps({
- layerGroup: nestedLayerGroup
+ const unexpectedNodeTitles = [
+ // Not visible since not expanded.
+ 'layer3',
+ // Not visible since it's the root node.
+ 'layerGroup'
+ ];
+
+ unexpectedNodeTitles.forEach(unexpectedNodeTitle => {
+ const node = screen.queryByText(unexpectedNodeTitle);
+ expect(node).not.toBeInTheDocument();
});
- expect((wrapper.instance() as LayerTree).olListenerKeys).toHaveLength(4);
});
- describe('
creation', () => {
+ it('accepts a layer group instead of getting all layers from the map directly', () => {
+ renderInMapContext(map,
+
+ );
- it('adds a for every child', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('.ant-tree-treenode');
+ const expectedNodeTitles = [
+ 'layer3'
+ ];
- expect(treeNodes).toHaveLength(4);
+ expectedNodeTitles.forEach(expectedNodeTitle => {
+ const node = screen.queryByText(expectedNodeTitle);
+ expect(node).toBeInTheDocument();
});
- // TODO This test could be better if the TreeNodes where iterable, but they
- // are not. See comment below.
- // it('can handle nested `ol.layer.group`s', () => {
- // const props = {
- // layerGroup,
- // map
- // };
- // const subLayer = new OlLayerTile({
- // name: 'subLayer',
- // source: new OlSourceTileWMS()
- // });
- // const nestedLayerGroup = new OlLayerGroup({
- // name: 'nestedLayerGroup',
- // layers: [subLayer]
- // });
- // layerGroup.getLayers().push(nestedLayerGroup);
-
- // const wrapper = TestUtil.mountComponent(LayerTree, props);
- // const treeNodes = wrapper.find('.ant-tree-treenode');
-
- // const groupNode = treeNodes.at(0);
- // const subNode = groupNode.props().children[0];
-
- // expect(subNode.props.title).toBe(subLayer.get('name'));
- // });
-
- it('can handle a replacement of layergroups `ol.Collection`', () => {
- const props = {
- map
- };
- const subLayer = new OlLayerTile({
- properties: {
- name: 'subLayer'
- },
- source: new OlSourceTileWMS()
- });
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const rebuildSpy = jest.spyOn((wrapper.instance() as LayerTree), 'rebuildTreeNodes');
- map.getLayerGroup().setLayers(new OlCollection([subLayer]));
- expect(rebuildSpy).toHaveBeenCalled();
- rebuildSpy.mockRestore();
+ const unexpectedNodeTitles = [
+ // Not visible since not contained in layerSubGroup
+ 'layer1',
+ 'layer2',
+ 'layerGroup',
+ // Not visible since it's the root node.
+ 'layerSubGroup'
+ ];
+
+ unexpectedNodeTitles.forEach(unexpectedNodeTitle => {
+ const node = screen.queryByText(unexpectedNodeTitle);
+ expect(node).not.toBeInTheDocument();
});
+ });
- it('sets the layer name as title per default', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('LayerTreeNode');
- treeNodes.forEach((node: any, index: number) => {
- const reverseIndex = treeNodes.length - (index + 1);
- const layer = layerGroup.getLayers().item(reverseIndex);
- expect(node.props().title).toBe(layer.get('name'));
- });
- });
+ it('removes all registered view listeners on unmount', () => {
+ expect(map.getListeners('moveend')).toBeUndefined();
- it('accepts a custom title renderer function', () => {
- const nodeTitleRenderer = function(layer: OlLayerBase) {
- return (
-
-
- {layer.get('name')}
-
-
-
- );
- };
- const props = {
- layerGroup,
- map,
- nodeTitleRenderer
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('LayerTreeNode');
-
- treeNodes.forEach((node: any, index: number) => {
- const reverseIndex = treeNodes.length - (index + 1);
- const layer = layerGroup.getLayers().item(reverseIndex);
- expect(node.find('span.span-1').length).toBe(1);
- expect(node.find('span.sub-span-1').length).toBe(1);
- expect(node.find('span.sub-span-1').props().children).toBe(layer.get('name'));
- expect(node.find('span.sub-span-2').length).toBe(1);
- });
- });
+ const {
+ unmount
+ } = renderInMapContext(map,
+
+ );
- it('can filter layers if a filterFunction is given', () => {
- const filterFunction = function(layer: OlLayerBase) {
- return layer.get('name') !== 'layer1';
- };
- const props = {
- layerGroup,
- map,
- filterFunction
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('.ant-tree-treenode');
-
- expect(treeNodes.length).toBe(3);
- });
+ expect(map.getView().getListeners('change:resolution')?.length).toBe(1);
- it('sets the right keys for the layers', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('LayerTreeNode');
-
- treeNodes.forEach((node: any, index: number) => {
- const reverseIndex = treeNodes.length - (index + 1);
- const layer = layerGroup.getLayers().item(reverseIndex);
- expect(node.props().eventKey).toBe(getUid(layer));
- });
- });
+ unmount();
- it('sets visible layers as checked', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('LayerTreeNode');
-
- treeNodes.forEach((node: any, index: number) => {
- const reverseIndex = treeNodes.length - (index + 1);
- const layer = layerGroup.getLayers().item(reverseIndex);
- expect(layer.getVisible()).toBe(node.props().checked);
- });
- });
+ expect(map.getView().getListeners('change:resolution')).toBeUndefined();
+ });
- describe('#treeNodeFromLayer', () => {
+ it('removes all registered layer(-group) listeners on unmount', () => {
+ expect(layer1.getListeners('change:visible')).toBeUndefined();
+ expect(layer2.getListeners('change:visible')).toBeUndefined();
+ expect(layer3.getListeners('change:visible')).toBeUndefined();
- it('returns a LayerTreeNode when called with a layer', () => {
- const props = {
- layerGroup,
- map
- };
- layerGroup.setVisible(false);
+ // The layer group has registered a default listener.
+ expect(layerGroup.getLayers().getListeners('add')?.length).toBe(1);
+ expect(layerGroup.getLayers().getListeners('remove')?.length).toBe(1);
+ expect(layerGroup.getListeners('change:layers')?.length).toBe(1);
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNode = (wrapper.instance() as LayerTree).treeNodeFromLayer(layer1);
+ expect(layerSubGroup.getLayers().getListeners('add')?.length).toBe(1);
+ expect(layerSubGroup.getLayers().getListeners('remove')?.length).toBe(1);
+ expect(layerSubGroup.getListeners('change:layers')?.length).toBe(1);
- expect(treeNode.props.title).toBe(layer1.get('name'));
- expect(treeNode.key).toBe(getUid(layer1));
- });
- });
+ const {
+ unmount
+ } = renderInMapContext(map,
+
+ );
+
+ expect(layer1.getListeners('change:visible')?.length).toBe(1);
+ expect(layer2.getListeners('change:visible')?.length).toBe(1);
+ expect(layer3.getListeners('change:visible')?.length).toBe(1);
+
+ expect(layerGroup.getLayers().getListeners('add')?.length).toBe(2);
+ expect(layerGroup.getLayers().getListeners('remove')?.length).toBe(2);
+ expect(layerGroup.getListeners('change:layers')?.length).toBe(2);
+
+ expect(layerSubGroup.getLayers().getListeners('add')?.length).toBe(2);
+ expect(layerSubGroup.getLayers().getListeners('remove')?.length).toBe(2);
+ expect(layerSubGroup.getListeners('change:layers')?.length).toBe(2);
+
+ unmount();
+
+ expect(layer1.getListeners('change:visible')).toBeUndefined();
+ expect(layer2.getListeners('change:visible')).toBeUndefined();
+ expect(layer3.getListeners('change:visible')).toBeUndefined();
+
+ expect(layerGroup.getLayers().getListeners('add')?.length).toBe(1);
+ expect(layerGroup.getLayers().getListeners('remove')?.length).toBe(1);
+ expect(layerGroup.getListeners('change:layers')?.length).toBe(1);
+
+ expect(layerSubGroup.getLayers().getListeners('add')?.length).toBe(1);
+ expect(layerSubGroup.getLayers().getListeners('remove')?.length).toBe(1);
+ expect(layerSubGroup.getListeners('change:layers')?.length).toBe(1);
});
- describe('#onCheck', () => {
- it('sets the correct visibility to the layer from the checked state', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const treeNodes = wrapper.find('LayerTreeNode');
-
-
- treeNodes.forEach((node: any, index: number) => {
- const reverseIndex = treeNodes.length - (index + 1);
- const layer = layerGroup.getLayers().item(reverseIndex);
- const checkBox = node.find('.ant-tree-checkbox');
- const wasVisible = layer.getVisible();
- checkBox.simulate('click');
- expect(wasVisible).toBe(!layer.getVisible());
- });
+ it('accepts a custom title renderer function', () => {
+ const nodeTitleRenderer = (layer: OlLayerBase) => {
+ return {`Custom-${layer.get('name')}`};
+ };
+
+ renderInMapContext(map,
+
+ );
+
+ const expectedNodeTitles = [
+ 'Custom-layer3'
+ ];
+
+ expectedNodeTitles.forEach(expectedNodeTitle => {
+ const node = screen.queryByText(expectedNodeTitle);
+ expect(node).toBeInTheDocument();
});
});
- describe('event handling', () => {
-
- it('sets checked state corresponding to layer.setVisible', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- let treeNode = wrapper.find('.ant-tree-treenode').at(3);
-
- expect(treeNode.find('.ant-tree-checkbox-checked').length).toEqual(1);
- act(() => {
- layer1.setVisible(false);
- });
- wrapper.update();
- treeNode = wrapper.find('.ant-tree-treenode').at(3);
- expect(treeNode.find('.ant-tree-checkbox-checked').length).toEqual(0);
- act(() => {
- layer1.setVisible(true);
- });
- wrapper.update();
- treeNode = wrapper.find('.ant-tree-treenode').at(3);
- expect(treeNode.find('.ant-tree-checkbox-checked').length).toEqual(1);
- });
+ it('accepts a filterFunction to filter the layers in the tree', () => {
+ const filterFunction = (layer: OlLayerBase) => {
+ return layer.get('name') === 'layer1';
+ };
- it('triggers tree rebuild on nodeTitleRenderer changes', () => {
- const exampleNodeTitleRenderer = function(layer: OlLayerBase) {
- return (
-
- {layer.get('name')}
-
- );
- };
-
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const rebuildSpy = jest.spyOn((wrapper.instance() as LayerTree), 'rebuildTreeNodes');
-
- wrapper.setProps({
- nodeTitleRenderer: exampleNodeTitleRenderer
- });
- expect(rebuildSpy).toHaveBeenCalledTimes(1);
-
- wrapper.setProps({
- nodeTitleRenderer: null
- });
- expect(rebuildSpy).toHaveBeenCalledTimes(2);
-
- rebuildSpy.mockRestore();
- });
+ renderInMapContext(map,
+
+ );
+ });
+
+ it('sets visible layers as checked initially', () => {
+ renderInMapContext(map,
+
+ );
+
+ const layer1Span = screen.queryByText('layer1');
+ const layer2Span = screen.queryByText('layer2');
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).toHaveClass('ant-tree-treenode-checkbox-checked');
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer2Span?.parentNode?.parentNode).not.toHaveClass('ant-tree-treenode-checkbox-checked');
+ });
+
+ it('sets the layers visibility on check', async () => {
+ renderInMapContext(map,
+
+ );
+
+ const layer1Span = screen.getByText('layer1');
+ const layer2Span = screen.getByText('layer2');
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).toHaveClass('ant-tree-treenode-checkbox-checked');
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer2Span?.parentNode?.parentNode).not.toHaveClass('ant-tree-treenode-checkbox-checked');
- it('triggers tree rebuild on layer add', () => {
- const props = {
- layerGroup,
- map
- };
- const layer = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const rebuildSpy = jest.spyOn((wrapper.instance() as LayerTree), 'rebuildTreeNodes');
-
- layerGroup.getLayers().push(layer);
- expect(rebuildSpy).toHaveBeenCalled();
-
- rebuildSpy.mockRestore();
+ expect(layer1.getVisible()).toBe(true);
+ expect(layer2.getVisible()).toBe(false);
+
+ await userEvent.click(layer1Span);
+ await userEvent.click(layer2Span);
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).not.toHaveClass('ant-tree-treenode-checkbox-checked');
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer2Span?.parentNode?.parentNode).toHaveClass('ant-tree-treenode-checkbox-checked');
+
+ expect(layer1.getVisible()).toBe(false);
+ expect(layer2.getVisible()).toBe(true);
+ });
+
+ it('updates the checked state if the layers visibility changes internally', async () => {
+ renderInMapContext(map,
+
+ );
+
+ const layer1Span = screen.getByText('layer1');
+ const layer2Span = screen.getByText('layer2');
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).toHaveClass('ant-tree-treenode-checkbox-checked');
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer2Span?.parentNode?.parentNode).not.toHaveClass('ant-tree-treenode-checkbox-checked');
+
+ act(() => {
+ layer1.setVisible(false);
+ layer2.setVisible(true);
});
- it('… also registers add/remove events for added groups ', () => {
- const props = {
- layerGroup,
- map
- };
- const layer = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const group = new OlLayerGroup({
- layers: [layer]
- });
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const rebuildSpy = jest.spyOn((wrapper.instance() as LayerTree), 'rebuildTreeNodes');
- const registerSpy = jest.spyOn((wrapper.instance() as LayerTree), 'registerAddRemoveListeners');
-
- layerGroup.getLayers().push(group);
- expect(rebuildSpy).toHaveBeenCalled();
- expect(registerSpy).toHaveBeenCalled();
-
- rebuildSpy.mockRestore();
- registerSpy.mockRestore();
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).not.toHaveClass('ant-tree-treenode-checkbox-checked');
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer2Span?.parentNode?.parentNode).toHaveClass('ant-tree-treenode-checkbox-checked');
+ });
+
+ it('sets the out-of-range class if the layer is not visible', () => {
+ renderInMapContext(map,
+
+ );
+
+ const layer1Span = screen.getByText('layer1');
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).toHaveClass('out-of-range');
+
+ act(() => {
+ map.getView().setZoom(10);
});
- it('trigger unregisterEventsByLayer and rebuildTreeNodes for removed layers ', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const rebuildSpy = jest.spyOn((wrapper.instance() as LayerTree), 'rebuildTreeNodes');
- const unregisterSpy = jest.spyOn((wrapper.instance() as LayerTree), 'unregisterEventsByLayer');
-
- layerGroup.getLayers().remove(layer1);
- expect(rebuildSpy).toHaveBeenCalled();
- expect(unregisterSpy).toHaveBeenCalled();
-
- rebuildSpy.mockRestore();
- unregisterSpy.mockRestore();
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(layer1Span?.parentNode?.parentNode).not.toHaveClass('out-of-range');
+ });
+
+ it('adds/removes layers to the tree if added/removed internally', () => {
+ renderInMapContext(map,
+
+ );
+
+ let newLayerSpan = screen.queryByText('newLayer');
+
+ expect(newLayerSpan).not.toBeInTheDocument();
+
+ const newLayer = new OlLayerTile({
+ source: new OlSourceTileWMS(),
+ properties: {
+ name: 'newLayer'
+ }
});
- it('… unregister recursively for removed groups', () => {
- const props = {
- layerGroup,
- map
- };
- const subLayer1 = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const subLayer2 = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const nestedLayerGroup = new OlLayerGroup({
- properties: {
- name: 'nestedLayerGroup'
- },
- layers: [subLayer1, subLayer2]
- });
- layerGroup.getLayers().push(nestedLayerGroup);
-
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const rebuildSpy = jest.spyOn((wrapper.instance() as LayerTree), 'rebuildTreeNodes');
- const unregisterSpy = jest.spyOn((wrapper.instance() as LayerTree), 'unregisterEventsByLayer');
-
- layerGroup.getLayers().remove(nestedLayerGroup);
- expect(rebuildSpy).toHaveBeenCalledTimes(1);
- expect(unregisterSpy).toHaveBeenCalledTimes(3);
-
- rebuildSpy.mockRestore();
- unregisterSpy.mockRestore();
+ act(() => {
+ map.addLayer(newLayer);
});
- describe('#unregisterEventsByLayer', () => {
-
- it('removes the listener and the eventKey from olListenerKeys', () => {
- const props = {
- layerGroup,
- map
- };
- const subLayer1 = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const subLayer2 = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const nestedLayerGroup = new OlLayerGroup({
- properties: {
- name: 'nestedLayerGroup'
- },
- layers: [subLayer1, subLayer2]
- });
- layerGroup.getLayers().push(nestedLayerGroup);
-
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const oldOlListenerKey = (wrapper.instance() as LayerTree).olListenerKeys;
- // @ts-ignore
- OlObservable.unByKey = jest.fn();
-
- (wrapper.instance() as LayerTree).unregisterEventsByLayer(subLayer1);
-
- const newOlListenerKey = (wrapper.instance() as LayerTree).olListenerKeys;
-
- expect(OlObservable.unByKey).toHaveBeenCalled();
- expect(newOlListenerKey.length).toBe(oldOlListenerKey.length - 1);
- });
-
- it('… of children for groups', () => {
- const props = {
- layerGroup,
- map
- };
- const subLayer1 = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const subLayer2 = new OlLayerTile({
- source: new OlSourceTileWMS()
- });
- const nestedLayerGroup = new OlLayerGroup({
- properties: {
- name: 'nestedLayerGroup'
- },
- layers: [subLayer1, subLayer2]
- });
- layerGroup.getLayers().push(nestedLayerGroup);
-
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- const oldOlListenerKey = (wrapper.instance() as LayerTree).olListenerKeys;
- // @ts-ignore
- OlObservable.unByKey = jest.fn();
-
- (wrapper.instance() as LayerTree).unregisterEventsByLayer(nestedLayerGroup);
-
- const newOlListenerKey = (wrapper.instance() as LayerTree).olListenerKeys;
-
- expect(OlObservable.unByKey).toHaveBeenCalledTimes(2);
- expect(newOlListenerKey.length).toBe(oldOlListenerKey.length - 2);
- });
+ newLayerSpan = screen.getByText('newLayer');
+
+ expect(newLayerSpan).toBeInTheDocument();
+ act(() => {
+ map.removeLayer(newLayer);
});
+ expect(newLayerSpan).not.toBeInTheDocument();
});
- describe('#setLayerVisibility', () => {
- it('sets the visibility of a single layer', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- layer1.setVisible(true);
-
- (wrapper.instance() as LayerTree).setLayerVisibility(layer1, false);
- expect(layer1.getVisible()).toBe(false);
- (wrapper.instance() as LayerTree).setLayerVisibility(layer1, true);
- expect(layer1.getVisible()).toBe(true);
- });
+ it('updates the layer name if changed internally', () => {
+ renderInMapContext(map,
+
+ );
- it('sets the visibility for the children when called with a layerGroup', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- layer1.setVisible(true);
- layer2.setVisible(true);
+ let layer1Span = screen.queryByText('layer1');
- (wrapper.instance() as LayerTree).setLayerVisibility(layerGroup, false);
- expect(layerGroup.getVisible()).toBe(false);
- expect(layer1.getVisible()).toBe(false);
- expect(layer2.getVisible()).toBe(false);
+ expect(layer1Span).toBeInTheDocument();
- (wrapper.instance() as LayerTree).setLayerVisibility(layerGroup, true);
- expect(layerGroup.getVisible()).toBe(true);
- expect(layer1.getVisible()).toBe(true);
- expect(layer2.getVisible()).toBe(true);
+ act(() => {
+ layer1.set('name', 'newLayerName');
});
- it('sets the parent layerGroups visible when layer has been made visible', () => {
- const props = {
- layerGroup,
- map
- };
- const wrapper = TestUtil.mountComponent(LayerTree, props);
- layerGroup.setVisible(false);
- layerSubGroup.setVisible(false);
- layer1.setVisible(false);
- layer2.setVisible(false);
- layer3.setVisible(false);
-
-
- (wrapper.instance() as LayerTree).setLayerVisibility(layer2, true);
- expect(layerGroup.getVisible()).toBe(true);
- expect(layer1.getVisible()).toBe(false);
- expect(layer2.getVisible()).toBe(true);
- expect(layer3.getVisible()).toBe(false);
- expect(layerSubGroup.getVisible()).toBe(false);
-
- (wrapper.instance() as LayerTree).setLayerVisibility(layer1, true);
- expect(layerGroup.getVisible()).toBe(true);
- expect(layer1.getVisible()).toBe(true);
- expect(layer2.getVisible()).toBe(true);
- expect(layer3.getVisible()).toBe(false);
- expect(layerSubGroup.getVisible()).toBe(false);
-
- layerGroup.setVisible(false);
- layerSubGroup.setVisible(false);
- layer1.setVisible(false);
- layer2.setVisible(false);
- layer3.setVisible(false);
-
- (wrapper.instance() as LayerTree).setLayerVisibility(layer3, true);
- expect(layer1.getVisible()).toBe(false);
- expect(layer2.getVisible()).toBe(false);
- expect(layer3.getVisible()).toBe(true);
- expect(layerSubGroup.getVisible()).toBe(true);
- expect(layerGroup.getVisible()).toBe(true);
- });
+ layer1Span = screen.queryByText('layer1');
+ expect(layer1Span).not.toBeInTheDocument();
+
+ layer1Span = screen.queryByText('newLayerName');
+
+ expect(layer1Span).toBeInTheDocument();
+ });
+
+ it('sets the visibility for the children when called with a layerGroup', async () => {
+ renderInMapContext(map,
+
+ );
+
+ const layerSubGroupSpan = screen.getByText('layerSubGroup');
+
+ expect(layer3.getVisible()).toBe(false);
+
+ await userEvent.click(layerSubGroupSpan);
+
+ expect(layer3.getVisible()).toBe(true);
});
- // TODO rc-tree drop event seems to be broken in PhantomJS / cant be simulated
- describe('#onDrop', () => {
-
- // let props = {};
- //
- // beforeEach(() => {
- // props = {
- // layerGroup,
- // map
- // };
- // const layer3 = new OlLayerTile({
- // name: 'layer3',
- // source: new OlSourceTileWMS()
- // });
- // const subLayer = new OlLayerTile({
- // name: 'subLayer',
- // source: new OlSourceTileWMS()
- // });
- // const nestedLayerGroup = new OlLayerGroup({
- // name: 'nestedLayerGroup',
- // layers: [subLayer]
- // });
- // props.layerGroup.getLayers().push(layer3);
- // props.layerGroup.getLayers().push(nestedLayerGroup);
- // });
- //
- // it('can handle drop on leaf', () => {
- // const wrapper = TestUtil.mountComponent(LayerTree, props);
- // const firstNode = wrapper.childAt(0);
- // const thirdNode = wrapper.childAt(2);
- // const dragTarget = firstNode.find('.ant-tree-node-content-wrapper');
- // const dropTarget = thirdNode.find('.react-geo-layertree-node');
- //
- // console.log(props.layerGroup.getLayers().getLength());
- // console.log(props.layerGroup.getLayers().item(0).get('name'));
- // console.log(props.layerGroup.getLayers().item(1).get('name'));
- // console.log(props.layerGroup.getLayers().item(2).get('name'));
- // console.log(props.layerGroup.getLayers().item(3).get('name'));
- //
- // debugger
- //
- // console.log('--------');
- // dragTarget.simulate('dragStart');
- // dropTarget.simulate('dragOver');
- // dropTarget.simulate('drop');
- //
- // console.log(props.layerGroup.getLayers().getLength());
- // console.log(props.layerGroup.getLayers().item(0).get('name'));
- // console.log(props.layerGroup.getLayers().item(1).get('name'));
- // console.log(props.layerGroup.getLayers().item(2).get('name'));
- // console.log(props.layerGroup.getLayers().item(3).get('name'));
- //
- //
- // });
-
- // it('can handle drop before leaf', () => {
- // const wrapper = TestUtil.mountComponent(LayerTree, props);
- // const treeNodes = wrapper.children('TreeNode');
- // const firstNode = treeNodes.get(0);
- // const thirdNode = treeNodes.get(2);
- // });
- //
- // it('can handle drop after leaf', () => {
- // const wrapper = TestUtil.mountComponent(LayerTree, props);
- // const treeNodes = wrapper.children('TreeNode');
- // const firstNode = treeNodes.get(0);
- // const thirdNode = treeNodes.get(2);
- // });
- //
- // it('can handle drop on folder', () => {
- // const wrapper = TestUtil.mountComponent(LayerTree, props);
- // const treeNodes = wrapper.children('TreeNode');
- // const firstNode = treeNodes.get(0);
- // const folderNode = treeNodes.get(3);
- // });
+ it('sets the parent layerGroup visible when layer has been made visible', async () => {
+ renderInMapContext(map,
+
+ );
+
+ // Expand all nodes.
+ const carets = screen.getAllByLabelText('caret-down');
+ for (const caret of carets) {
+ await userEvent.click(caret);
+ }
+
+ const layerSubGroupSpan = screen.getByText('layerSubGroup');
+
+ // eslint-disable-next-line testing-library/no-node-access
+ let layerSubGroupCheckbox = layerSubGroupSpan?.parentNode?.parentNode?.querySelector('.ant-tree-checkbox-checked');
+
+ expect(layerSubGroupCheckbox).toBe(null);
+
+ const layer3Span = screen.getByText('layer3');
+
+ await userEvent.click(layer3Span);
+
+ // eslint-disable-next-line testing-library/no-node-access
+ layerSubGroupCheckbox = layerSubGroupSpan?.parentNode?.parentNode?.querySelector('.ant-tree-checkbox-checked');
+
+ expect(layerSubGroupCheckbox).toBeInstanceOf(HTMLSpanElement);
+ });
+
+ it('renders the layers in correct order', () => {
+ renderInMapContext(map,
+
+ );
+
+ // Last drawn map layer (layerSubGroup) should be on top of the tree.
+ const layerSubGroupSpan = screen.getByText('layerSubGroup');
+ const layer2Span = screen.getByText('layer2');
+ const layer1Span = screen.getByText('layer1');
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
+ expect(layerSubGroupSpan.compareDocumentPosition(layer2Span)).toEqual(4);
+ expect(layer2Span.compareDocumentPosition(layer1Span)).toEqual(4);
+ });
+
+ it('can handle drop on a node', () => {
+ renderInMapContext(map,
+
+ );
+
+ const layers = map.getLayers().getArray();
+
+ expect(layers[0]).toBe(layer1);
+ expect(layers[1]).toBe(layer2);
+ expect(layers[2]).toBe(layerSubGroup);
+
+ const layerSubGroupSpan = screen.getByText('layerSubGroup');
+ const layer1Span = screen.getByText('layer1');
+
+ // Move layer1 on layerSubGroup (relative position = 0).
+ fireEvent.dragStart(layer1Span);
+ fireEvent.dragEnter(layerSubGroupSpan);
+ fireEvent.dragOver(layerSubGroupSpan);
+ fireEvent.drop(layerSubGroupSpan);
+
+ expect(layers[0]).toBe(layer2);
+ expect(layers[1]).toBe(layerSubGroup);
+ expect((layers[1] as OlLayerGroup).getLayers().getArray()).toContain(layer1);
+ });
+
+ it('ignores drop on leaf', () => {
+ renderInMapContext(map,
+
+ );
+
+ const layers = map.getLayers().getArray();
+
+ expect(layers[0]).toBe(layer1);
+ expect(layers[1]).toBe(layer2);
+ expect(layers[2]).toBe(layerSubGroup);
+
+ const layer2Span = screen.getByText('layer2');
+ const layer1Span = screen.getByText('layer1');
+
+ // Move layer1 on layer2 (relative position = 0).
+ fireEvent.dragStart(layer1Span);
+ fireEvent.dragEnter(layer2Span);
+ fireEvent.dragOver(layer2Span);
+ fireEvent.drop(layer2Span);
+
+ expect(layers[0]).toBe(layer1);
+ expect(layers[1]).toBe(layer2);
+ expect(layers[2]).toBe(layerSubGroup);
+ });
+
+ it('can handle drop after leaf', () => {
+ renderInMapContext(map,
+
+ );
+
+ const layers = map.getLayers().getArray();
+
+ expect(layers[0]).toBe(layer1);
+ expect(layers[1]).toBe(layer2);
+ expect(layers[2]).toBe(layerSubGroup);
+
+ const layerSubGroupSpan = screen.getByText('layerSubGroup');
+ const layer2Span = screen.getByText('layer2');
+
+ // Move layerSubGroup on bottom of layer2 (relative position = 1).
+ fireEvent.dragStart(layerSubGroupSpan);
+ fireEvent.dragEnter(layer2Span);
+ fireEvent.dragOver(layer2Span);
+ fireEvent.drop(layer2Span);
+
+ expect(layers[0]).toBe(layer1);
+ expect(layers[1]).toBe(layerSubGroup);
+ expect(layers[2]).toBe(layer2);
});
+ // TODO This seems not to working with the jest runner.
+ // it('can handle drop before leaf', () => {
+ // renderInMapContext(map,
+ //
+ // );
+
+ // const layers = map.getLayers().getArray();
+
+ // expect(layers[0]).toBe(layer1);
+ // expect(layers[1]).toBe(layer2);
+ // expect(layers[2]).toBe(layerSubGroup);
+
+ // const layerSubGroupSpan = screen.getByText('layerSubGroup');
+ // const layer2Span = screen.getByText('layer2');
+ // const layer1Span = screen.getByText('layer1');
+
+ // // Move layer1 on top of layer2 (relative position = 1).
+ // fireEvent.dragStart(layer1Span);
+ // fireEvent.dragEnter(layer2Span);
+ // fireEvent.dragOver(layer2Span);
+ // // fireEvent.dragLeave(layer2Span);
+ // // fireEvent.dragExit(layer2Span);
+ // fireEvent.dragEnter(layerSubGroupSpan);
+ // // fireEvent.dragOver(layer2Span);
+ // fireEvent.drop(layerSubGroupSpan);
+
+ // // expect(layers[0]).toBe(layer2);
+ // // expect(layers[1]).toBe(layer1);
+ // // expect(layers[2]).toBe(layerSubGroup);
+ // });
});
diff --git a/src/LayerTree/LayerTree.tsx b/src/LayerTree/LayerTree.tsx
index 8be72572bb..3b58177a0c 100644
--- a/src/LayerTree/LayerTree.tsx
+++ b/src/LayerTree/LayerTree.tsx
@@ -1,31 +1,47 @@
+import './LayerTree.less';
+
import Logger from '@terrestris/base-util/dist/Logger';
import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil';
-import { Tree } from 'antd';
+import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap';
+import {
+ Tree,
+ TreeDataNode
+} from 'antd';
import {
- AntTreeNodeCheckedEvent,
+ BasicDataNode,
DataNode,
TreeProps} from 'antd/lib/tree';
-import { AntTreeNodeDropEvent, AntTreeNodeSelectedEvent } from 'antd/lib/tree/Tree';
import _isEqual from 'lodash/isEqual';
import _isFunction from 'lodash/isFunction';
import _isNumber from 'lodash/isNumber';
import { getUid } from 'ol';
-import OlCollection from 'ol/Collection';
-import { EventsKey as OlEventsKey } from 'ol/events';
+import {
+ EventsKey as OlEventsKey,
+} from 'ol/events';
import OlLayerBase from 'ol/layer/Base';
import OlLayerGroup from 'ol/layer/Group';
-import OlMap from 'ol/Map';
-import OlMapEvent from 'ol/MapEvent';
-import { unByKey } from 'ol/Observable';
+import {
+ unByKey
+} from 'ol/Observable';
+import {
+ NodeDragEventParams
+} from 'rc-tree/lib/contextTypes';
import {
EventDataNode
} from 'rc-tree/lib/interface';
-import * as React from 'react';
-import { ReactElement, ReactNode } from 'react';
+import {
+ AllowDropOptions,
+ CheckInfo
+} from 'rc-tree/lib/Tree';
+import React, {
+ Key,
+ useCallback,
+ useEffect,
+ useRef,
+ useState
+} from 'react';
import { CSS_PREFIX } from '../constants';
-import LayerTreeNode, { LayerTreeNodeProps } from './LayerTreeNode/LayerTreeNode';
-
interface OwnProps {
/**
@@ -35,618 +51,342 @@ interface OwnProps {
*
* Compare MDN Docs for Array.prototype.filter: https://mdn.io/array/filter
*/
- filterFunction: (value: any, index: number, array: any[]) => boolean;
- /**
- * An optional CSS class which should be added.
- */
- className?: string;
+ filterFunction?: (value: OlLayerBase, index: number, array: OlLayerBase[]) => boolean;
/**
* A LayerGroup the Tree should handle.
*/
layerGroup?: OlLayerGroup;
- /**
- * The OpenLayers map the tree interacts with.
- */
- map: OlMap;
-
/**
* A function that can be used to pass a custom node title. It can return
* any renderable element (String, Number, Element etc.) and receives
* the layer instance of the current tree node.
*/
nodeTitleRenderer?: (layer: OlLayerBase) => React.ReactNode;
-
- toggleOnClick?: boolean;
}
-interface LayerTreeState {
- layerGroup: OlLayerGroup | null;
- layerGroupRevision: number;
- treeNodes: ReactElement[];
- checkedKeys: React.ReactText[];
- mapResolution: number;
-}
+const defaultClassName = `${CSS_PREFIX}layertree`;
-export type LayerTreeProps = OwnProps & TreeProps;
+export type LayerTreeProps = OwnProps & TreeProps;
-/**
- * The LayerTree.
- *
- * Note. This component expects that all layerGroups are permanently visible.
- *
- * @class LayerTree
- * @extends React.Component
- */
-class LayerTree extends React.Component {
+const LayerTree: React.FC = ({
+ className,
+ layerGroup,
+ nodeTitleRenderer,
+ filterFunction,
+ checkable = true,
+ draggable = true,
+ ...passThroughProps
+}) => {
+ const [checkedKeys, setCheckedKeys] = useState([]);
+ const [treeData, setTreeData] = useState([]);
- /**
- * The default properties.
- */
- static defaultProps = {
- draggable: true,
- checkable: true,
- filterFunction: () => true,
- toggleOnClick: false
- };
+ const olListenerKeys = useRef([]);
- /**
- * The className added to this component.
- * @private
- */
- className = `${CSS_PREFIX}layertree`;
+ const map = useMap();
- /**
- * An array of ol.EventsKey as returned by on() or once().
- * @private
- */
- olListenerKeys: OlEventsKey[] = [];
+ const getVisibleLayerKeys = useCallback(() => {
+ if (!map) {
+ return [];
+ }
- /**
- * Create the LayerTree.
- *
- * @constructs LayerTree
- */
- constructor(props: LayerTreeProps) {
- super(props);
-
- this.state = {
- layerGroup: null,
- layerGroupRevision: 0,
- treeNodes: [],
- checkedKeys: [],
- mapResolution: -1
- };
- }
+ const lGroup = layerGroup ? MapUtil.getAllLayers(layerGroup) : MapUtil.getAllLayers(map);
- /**
- * Invoked after the component is instantiated as well as when it
- * receives new props. It should return an object to update state, or null
- * to indicate that the new props do not require any state updates.
- *
- * @param nextProps The next properties.
- * @param prevState The previous state.
- */
- static getDerivedStateFromProps(nextProps: LayerTreeProps, prevState: LayerTreeState) {
- if (prevState.layerGroup && nextProps.layerGroup) {
- if (!_isEqual(getUid(prevState.layerGroup), getUid(nextProps.layerGroup)) ||
- !_isEqual(prevState.layerGroupRevision, nextProps.layerGroup.getRevision())) {
- return {
- layerGroup: nextProps.layerGroup,
- layerGroupRevision: nextProps.layerGroup.getRevision()
- };
- }
+ let visibleLayers = lGroup
+ .filter(layer => !(layer instanceof OlLayerGroup) && layer.getVisible());
+
+ if (filterFunction) {
+ visibleLayers = visibleLayers.filter(filterFunction);
}
- return null;
- }
- /**
- * Determines what to do on the initial mount.
- */
- componentDidMount() {
- const layerGroup = this.props.layerGroup ?
- this.props.layerGroup :
- this.props.map.getLayerGroup();
-
- const revision = this.props.layerGroup ? this.props.layerGroup.getRevision() : 0;
-
- this.setState({
- layerGroup,
- layerGroupRevision: revision
- }, () => {
- if (this.state.layerGroup) {
- this.registerAddRemoveListeners(this.state.layerGroup);
- }
- this.registerResolutionChangeHandler();
- this.rebuildTreeNodes();
- });
- }
+ const visibleKeys = visibleLayers.map(getUid);
- /**
- * Invoked immediately after updating occurs. This method is not called for
- * the initial render.
- *
- * @param prevProps The previous props.
- * @param prevState The previous state.
- */
- componentDidUpdate(prevProps: LayerTreeProps, prevState: LayerTreeState) {
- const {
- layerGroup,
- nodeTitleRenderer
- } = this.props;
-
- if (layerGroup && prevState.layerGroup) {
- if (!_isEqual(getUid(prevState.layerGroup), getUid(layerGroup))) {
- unByKey(this.olListenerKeys);
- this.olListenerKeys = [];
-
- this.registerAddRemoveListeners(layerGroup);
- this.rebuildTreeNodes();
- }
- }
+ return visibleKeys;
+ }, [filterFunction, layerGroup, map]);
- if (nodeTitleRenderer !== prevProps.nodeTitleRenderer) {
- this.rebuildTreeNodes();
+ const getTreeNodeTitle = useCallback((layer: OlLayerBase) => {
+ if (_isFunction(nodeTitleRenderer)) {
+ return nodeTitleRenderer(layer);
+ } else {
+ return layer.get('name');
}
- }
+ }, [nodeTitleRenderer]);
- /**
- * Determines what to do when the component is unmounted.
- */
- componentWillUnmount() {
- unByKey(this.olListenerKeys);
- }
-
- /**
- * Creates TreeNodes from a given layergroup and sets the treeNodes in the state.
- *
- * @param groupLayer A grouplayer.
- */
- treeNodesFromLayerGroup(groupLayer: OlLayerGroup) {
- const layerArray = groupLayer.getLayers().getArray()
- .filter(this.props.filterFunction);
- const treeNodes = layerArray.map((layer) => {
- return this.treeNodeFromLayer(layer);
+ const hasListener = useCallback((key: OlEventsKey) => {
+ return olListenerKeys.current.some(listenerKey => {
+ return listenerKey.target === key.target
+ && listenerKey.type === key.type
+ && listenerKey.listener === key.listener;
});
- treeNodes.reverse();
- this.setState({ treeNodes });
- }
+ }, []);
- /**
- * Registers the add/remove listeners recursively for all ol.layer.Group.
- *
- * @param groupLayer A ol.layer.Group
- */
- registerAddRemoveListeners(groupLayer: OlLayerGroup) {
- const collection = groupLayer.getLayers();
- const addEvtKey = collection.on('add', this.onCollectionAdd);
- const removeEvtKey = collection.on('remove', this.onCollectionRemove);
- const changeEvtKey = groupLayer.on('change:layers', this.onChangeLayers);
+ const treeNodeFromLayer = useCallback((layer: OlLayerBase) => {
+ let childNodes: TreeDataNode[] = [];
- this.olListenerKeys.push(addEvtKey, removeEvtKey, changeEvtKey);
+ if (filterFunction && [layer].filter(filterFunction).length === 0) {
+ return;
+ }
- collection.forEach((layer) => {
- if ((layer as OlLayerGroup).getLayers) {
- this.registerAddRemoveListeners((layer as OlLayerGroup));
+ if (layer instanceof OlLayerGroup) {
+ let childLayers = layer.getLayers().getArray();
+ if (filterFunction) {
+ childLayers = childLayers.filter(filterFunction);
}
- });
- }
-
- /**
- * Registers an eventhandler on the `ol.View`, which will rebuild the tree
- * nodes whenever the view's resolution changes.
- */
- registerResolutionChangeHandler() {
- const { map } = this.props;
- const evtKey = map.on('moveend', this.rebuildTreeNodes.bind(this));
- this.olListenerKeys.push(evtKey); // TODO when and how to we unbind?
- }
+ childNodes = childLayers
+ .map(childLayer => treeNodeFromLayer(childLayer))
+ .filter(childLayer => childLayer !== undefined)
+ .toReversed() as TreeDataNode[];
+ }
- /**
- * Listens to the collections add event of a collection.
- * Registers add/remove listeners if element is a collection and rebuilds the
- * treeNodes.
- *
- * @param evt The add event.
- */
- onCollectionAdd = (evt: any) => {
- if ((evt.element as OlLayerGroup).getLayers) {
- this.registerAddRemoveListeners(evt.element);
+ return {
+ key: getUid(layer),
+ title: getTreeNodeTitle(layer),
+ className: MapUtil.layerInResolutionRange(layer, map) ? '' : 'out-of-range',
+ // Required to identify a group layer/node.
+ children: layer instanceof OlLayerGroup ? childNodes : undefined
+ } as TreeDataNode;
+ }, [map, getTreeNodeTitle, filterFunction]);
+
+ const treeNodesFromLayerGroup = useCallback(() => {
+ if (!map) {
+ return [];
}
- this.rebuildTreeNodes();
- };
- /**
- * Listens to the collections remove event of a collection.
- * Unregisters the events of deleted layers and rebuilds the treeNodes.
- *
- * @param evt The remove event.
- */
- onCollectionRemove = (evt: any) => {
- this.unregisterEventsByLayer(evt.element);
- if ((evt.element as OlLayerGroup).getLayers) {
- evt.element.getLayers().forEach((layer: OlLayerBase) => {
- this.unregisterEventsByLayer(layer);
+ const lGroup = layerGroup ? layerGroup : map.getLayerGroup();
+
+ return lGroup.getLayers().getArray()
+ .map(l => treeNodeFromLayer(l))
+ .filter(n => n !== undefined)
+ .toReversed() as TreeDataNode[];
+ }, [layerGroup, map, treeNodeFromLayer]);
+
+ const onChangeResolution = useCallback(() => {
+ setTreeData(treeNodesFromLayerGroup());
+ }, [treeNodesFromLayerGroup]);
+
+ const setLayerVisibility = useCallback((layer: OlLayerBase, visible: boolean) => {
+ if (layer instanceof OlLayerGroup) {
+ layer.getLayers().forEach(subLayer => {
+ setLayerVisibility(subLayer, visible);
});
+ } else {
+ layer.setVisible(visible);
}
- this.rebuildTreeNodes();
- };
+ }, []);
- /**
- * Listens to the LayerGroups change:layers event.
- * Unregisters the old and reregisters new listeners.
- *
- * @param evt The change event.
- */
- onChangeLayers = (evt: any) => {
- this.unregisterEventsByLayer(evt.oldValue);
- if (evt.oldValue instanceof OlCollection) {
- evt.oldValue.forEach((layer: OlLayerBase) => this.unregisterEventsByLayer(layer));
- }
- if (evt.target.getLayers) {
- this.registerAddRemoveListeners(evt.target);
- }
- this.rebuildTreeNodes();
- };
+ const updateTreeNodes = useCallback(() => {
+ setTreeData(treeNodesFromLayerGroup());
+ }, [treeNodesFromLayerGroup]);
- /**
- * Unregisters the Events of a given layer.
- *
- * @param layer An ol.layer.Base.
- */
- unregisterEventsByLayer = (layer: OlLayerBase) => {
- this.olListenerKeys = this.olListenerKeys.filter((key) => {
- if ((layer as OlLayerGroup).getLayers) {
- const layers = (layer as OlLayerGroup).getLayers();
- if (key.target === layers) {
- if ((key.type === 'add' && key.listener === this.onCollectionAdd) ||
- (key.type === 'remove' && key.listener === this.onCollectionRemove) ||
- (key.type === 'change:layers' && key.listener === this.onChangeLayers)) {
-
- unByKey(key);
- return false;
- }
- }
- } else if (key.target === layer) {
- if (key.type === 'change:visible' && key.listener === this.onLayerChangeVisible) {
- unByKey(key);
- return false;
- }
- }
- return true;
- });
- };
+ const updateCheckedKeys = useCallback(() => {
+ setCheckedKeys(getVisibleLayerKeys());
+ }, [getVisibleLayerKeys]);
- /**
- * Rebuilds the treeNodes and its checked states.
- * @param evt The OpenLayers MapEvent (passed by moveend)
- *
- */
- rebuildTreeNodes = (evt?: OlMapEvent) => {
- const { mapResolution } = this.state;
+ const updateTree = useCallback(() => {
+ updateTreeNodes();
+ updateCheckedKeys();
+ }, [updateTreeNodes, updateCheckedKeys]);
- let newMapResolution: number = -1;
+ useEffect(() => {
+ updateTree();
+ }, [updateTree]);
- if (evt?.target instanceof OlMap) {
- newMapResolution = evt.target.getView().getResolution() ?? -1;
- if (mapResolution === newMapResolution) {
- // If map resolution didn't change => no redraw of tree nodes needed.
- return;
- }
+ const registerLayerListeners = useCallback((layer: OlLayerBase) => {
+ if (filterFunction && [layer].filter(filterFunction).length === 0) {
+ return;
}
- if (this.state.layerGroup) {
- this.treeNodesFromLayerGroup(this.state.layerGroup);
+ if (!(hasListener({ target: layer, type: 'propertychange', listener: updateTree }))) {
+ const evtKey = layer.on('propertychange', updateTree);
+ olListenerKeys.current.push(evtKey);
}
- const checkedKeys = this.getVisibleOlUids();
- this.setState({
- checkedKeys,
- mapResolution: newMapResolution
- });
- };
+ if (layer instanceof OlLayerGroup) {
+ const layerCollection = layer.getLayers();
- /**
- * Returns the title to render in the LayerTreeNode. If a nodeTitleRenderer
- * has been passed as prop, it will be called and the (custom) return value
- * will be rendered. Note: This can be any renderable element collection! If
- * no function is given (the default) the layer name will be passed.
- *
- * @param layer The layer attached to the tree node.
- * @return The title composition to render.
- */
- getTreeNodeTitle(layer: OlLayerBase) {
- if (_isFunction(this.props.nodeTitleRenderer)) {
- return this.props.nodeTitleRenderer.call(this, layer);
- } else {
- return layer.get('name');
- }
- }
+ if (!(hasListener({ target: layerCollection, type: 'add', listener: updateTree }))) {
+ const addEvtKey = layerCollection.on('add', updateTree);
+ olListenerKeys.current.push(addEvtKey);
+ }
+ if (!(hasListener({ target: layerCollection, type: 'remove', listener: updateTree }))) {
+ const removeEvtKey = layerCollection.on('remove', updateTree);
+ olListenerKeys.current.push(removeEvtKey);
+ }
+ if (!(hasListener({ target: layer, type: 'change:layers', listener: updateTree }))) {
+ const changeEvtKey = layer.on('change:layers', updateTree);
+ olListenerKeys.current.push(changeEvtKey);
+ }
- /**
- * Creates a treeNode from a given layer.
- *
- * @param layer The given layer.
- * @return The corresponding LayerTreeNode Element.
- */
- treeNodeFromLayer(layer: OlLayerBase): ReactElement {
- let childNodes: ReactNode = null;
-
- if ((layer as OlLayerGroup).getLayers) {
- const childLayers = (layer as OlLayerGroup).getLayers().getArray()
- .filter(this.props.filterFunction);
- childNodes = childLayers.map((childLayer: OlLayerBase) => {
- return this.treeNodeFromLayer(childLayer);
- }).reverse();
+ for (const lay of layerCollection.getArray()) {
+ registerLayerListeners(lay);
+ }
} else {
- if (!this.hasListener({ target: layer, type: 'change:visible', listener: this.onLayerChangeVisible })) {
- const eventKey = layer.on('change:visible', this.onLayerChangeVisible);
- this.olListenerKeys.push(eventKey);
+ if (!(hasListener({ target: layer, type: 'change:visible', listener: updateCheckedKeys }))) {
+ const evtKey = layer.on('change:visible', updateCheckedKeys);
+ olListenerKeys.current.push(evtKey);
}
}
+ }, [filterFunction, hasListener, updateTree, updateCheckedKeys]);
- return (
-
- {childNodes}
-
- );
- }
+ const registerAllLayerListeners = useCallback(() => {
+ if (!map) {
+ return;
+ }
- /**
- * Determines if the target has already registered the given listener for the
- * given eventtype.
- */
- hasListener = (key: OlEventsKey) => {
- return this.olListenerKeys.some((listenerKey) => {
- return listenerKey.target === key.target
- && listenerKey.type === key.type
- && listenerKey.listener === key.listener;
- });
- };
+ const lGroup = layerGroup ? layerGroup : map.getLayerGroup();
- /**
- * Reacts to the layer change:visible event and calls setCheckedState.
- */
- onLayerChangeVisible = () => {
- const checkedKeys = this.getVisibleOlUids();
- this.setState({
- checkedKeys
- }, () => {
- this.rebuildTreeNodes();
- });
- };
+ registerLayerListeners(lGroup);
- /**
- * Get the flat array of ol_uids from visible non groupLayers.
- *
- * @return The visible ol_uids.
- */
- getVisibleOlUids = (): string[] => {
- if (!this.state.layerGroup) {
- return [];
- }
+ }, [layerGroup, map, registerLayerListeners]);
- return MapUtil.getAllLayers(this.state.layerGroup,
- (layer: OlLayerBase) => {
- return !((layer as OlLayerGroup).getLayers) && layer.getVisible();
- })
- .filter(this.props.filterFunction)
- .map(getUid);
- };
+ const unregisterAllLayerListeners = useCallback(() => {
+ unByKey(olListenerKeys.current);
+ }, []);
- /**
- * Sets the visibility of a layer due to its checked state.
- *
- * @param checkedKeys Contains all checkedKeys.
- * @param e The ant-tree event object for this event. See ant docs.
- */
- onCheck(checkedKeys: string[], e: AntTreeNodeCheckedEvent) {
- const { checked = false } = e;
- const eventKey = e.node.props.eventKey;
- if (eventKey) {
- const layer = MapUtil.getLayerByOlUid(this.props.map, eventKey);
- if (!layer) {
- Logger.error('Layer is not defined');
- return;
- }
- this.setLayerVisibility(layer, checked);
+ useEffect(() => {
+ registerAllLayerListeners();
+
+ return () => {
+ unregisterAllLayerListeners();
+ };
+ }, [registerAllLayerListeners, unregisterAllLayerListeners]);
+
+ // Reregister all layer listeners if the treeData changes, this is e.g. required if a layer becomes
+ // a child of a layer group that isn't part of the treeData yet.
+ useEffect(() => {
+ unregisterAllLayerListeners();
+ registerAllLayerListeners();
+ }, [treeData, registerAllLayerListeners, unregisterAllLayerListeners]);
+
+ useEffect(() => {
+ if (!map) {
+ return;
}
- }
- /**
- * Toggles the visibility of a layer when clicking the name.
- *
- * @param checkedKeys Contains all checkedKeys.
- * @param e The ant-tree event object for this event. See ant docs.
- */
- onSelect = (checkedKeys: string[], e: AntTreeNodeSelectedEvent) => {
- // @ts-ignore
- const checked = !e.node.checked;
- // @ts-ignore
- e.node.checked = checked;
- const eventKey = e.node.props.eventKey;
- if (eventKey) {
- const layer = MapUtil.getLayerByOlUid(this.props.map, eventKey);
- if (!layer) {
- Logger.error('layer is not defined');
- return;
- }
- this.setLayerVisibility(layer, checked);
+ map.getView().on('change:resolution', onChangeResolution);
+
+ return () => {
+ map.getView().un('change:resolution', onChangeResolution);
+ };
+ }, [map, onChangeResolution]);
+
+ const onCheck = (keys: {
+ checked: Key[];
+ halfChecked: Key[];
+ } | Key[], info: CheckInfo) => {
+ if (!map) {
+ return;
}
- };
- /**
- * Sets the layer visibility. Calls itself recursively for groupLayers.
- *
- * @param layer The layer.
- * @param visibility The visibility.
- */
- setLayerVisibility(layer: OlLayerBase, visibility: boolean) {
- if ((layer as OlLayerGroup).getLayers) {
- layer.setVisible(visibility);
- (layer as OlLayerGroup).getLayers().forEach((subLayer) => {
- this.setLayerVisibility(subLayer, visibility);
- });
- } else {
- layer.setVisible(visibility);
- // if layer has a parent folder, make it visible too
- if (visibility) {
- const group = this.props.layerGroup ? this.props.layerGroup :
- this.props.map.getLayerGroup();
- this.setParentFoldersVisible(group, getUid(layer), group);
- }
+ const key = info.node.key as string;
+ const checked = info.checked;
+
+ if (!key) {
+ return;
}
- }
- /**
- * Find the parent OlLayerGroup for the given layers ol_uid and make it
- * visible. Traverse the tree to also set the parenting layer groups visible
- *
- * @param currentGroup The current group to search in
- * @param olUid The ol_uid of the layer or folder that has been set visible
- * @param mainGroup The main group to search in. Needed when searching for
- * parents as we always have to start search from top
- */
- setParentFoldersVisible(currentGroup: OlLayerGroup, olUid: string, mainGroup: OlLayerGroup) {
- const items = currentGroup.getLayers().getArray();
- const groups = items.filter(l => l instanceof OlLayerGroup) as OlLayerGroup[];
- const match = items.find(i => getUid(i) === olUid);
- if (match) {
- currentGroup.setVisible(true);
- this.setParentFoldersVisible(mainGroup, getUid(currentGroup), mainGroup);
+ const layer = MapUtil.getLayerByOlUid(map, key);
+
+ if (!layer) {
+ Logger.error('Layer is not defined');
return;
}
- groups.forEach(g => {
- this.setParentFoldersVisible(g, olUid, mainGroup);
- });
- }
- /**
- * The callback method for the drop event. Layers will get reordered in the map
- * and the tree.
- *
- * @param e The ant-tree event object for this event. See ant docs.
- */
- onDrop(e: AntTreeNodeDropEvent) {
- if (!e.dragNode.props.eventKey || !e.node.props.eventKey) {
+ setLayerVisibility(layer, checked);
+ };
+
+ const onDrop = (info: NodeDragEventParams & {
+ dragNode: EventDataNode;
+ dragNodesKeys: Key[];
+ dropPosition: number;
+ dropToGap: boolean;
+ }) => {
+ const dropKey = info.node.key as string;
+ const dragKey = info.dragNode.key as string;
+ const dropPos = info.node.pos.split('-');
+ const dropPosition = info.dropPosition;
+ // The drop position relative to the drop node, inside 0, top -1, bottom 1.
+ const dropPositionRelative = dropPosition - parseInt(dropPos[dropPos.length - 1], 10);
+
+ // Reorder layers
+ if (!map) {
return;
}
- const dragLayer = MapUtil.getLayerByOlUid(this.props.map, e.dragNode.props.eventKey);
+
+ const dragLayer = MapUtil.getLayerByOlUid(map, dragKey);
if (!dragLayer) {
Logger.error('dragLayer is not defined');
return;
}
- const dragInfo = MapUtil.getLayerPositionInfo(dragLayer, this.props.map);
- if (!dragInfo || !dragInfo?.groupLayer) {
- return;
- }
- const dragCollection = dragInfo.groupLayer.getLayers();
- const dropLayer = MapUtil.getLayerByOlUid(this.props.map, e.node.props.eventKey);
+
+ const dropLayer = MapUtil.getLayerByOlUid(map, dropKey);
if (!dropLayer) {
Logger.error('dropLayer is not defined');
return;
}
- const dropPos = e.node.props.pos.split('-');
- const location = e.dropPosition - Number(dropPos[dropPos.length - 1]);
- dragCollection.remove(dragLayer);
+ const dragInfo = MapUtil.getLayerPositionInfo(dragLayer, map);
+ if (!dragInfo || !dragInfo?.groupLayer) {
+ return;
+ }
- const dropInfo = MapUtil.getLayerPositionInfo(dropLayer, this.props.map);
+ const dropInfo = MapUtil.getLayerPositionInfo(dropLayer, map);
if (!dropInfo || !dropInfo?.groupLayer) {
return;
}
- const dropPosition = dropInfo.position;
+
+ const dragCollection = dragInfo.groupLayer.getLayers();
const dropCollection = dropInfo.groupLayer.getLayers();
- if (!_isNumber(dropPosition)) {
- return;
- }
+ dragCollection.remove(dragLayer);
- // drop before node
- if (location === -1) {
+ const dropLayerIndex = dropCollection.getArray().findIndex(l => l === dropLayer);
+
+ // Drop on the top of the drop node/layer.
+ if (dropPositionRelative === -1) {
if (dropPosition === dropCollection.getLength() - 1) {
dropCollection.push(dragLayer);
} else {
- dropCollection.insertAt(dropPosition + 1, dragLayer);
+ dropCollection.insertAt(dropLayerIndex + 1, dragLayer);
}
- // drop on node
- } else if (location === 0) {
- if ((dropLayer as OlLayerGroup).getLayers) {
- (dropLayer as OlLayerGroup).getLayers().push(dragLayer);
- } else {
- dropCollection.insertAt(dropPosition + 1, dragLayer);
+ // Drop on node (= to a layer group).
+ } else if (dropPositionRelative === 0) {
+ if (dropLayer instanceof OlLayerGroup) {
+ dropLayer.getLayers().push(dragLayer);
}
- // drop after node
- } else if (location === 1) {
- dropCollection.insertAt(dropPosition, dragLayer);
+ // Drop on the bottom of the drop node/layer.
+ } else if (dropPositionRelative === 1) {
+ dropCollection.insertAt(dropLayerIndex, dragLayer);
}
+ };
- this.rebuildTreeNodes();
- }
-
- /**
- * Call rebuildTreeNodes onExpand to avoid sync issues.
- *
- */
- onExpand = (expandedKeys: string[], info: {
- node: EventDataNode;
- expanded: boolean;
- nativeEvent: MouseEvent;
- }) => {
- const {
- onExpand
- } = this.props;
-
- this.rebuildTreeNodes();
+ const allowDrop = (options: AllowDropOptions) => {
+ const dropNode = options.dropNode;
+ const dropPositionRelative = options.dropPosition;
- if (onExpand) {
- onExpand(expandedKeys, info);
- }
+ // Don't allow dropping on a layer node.
+ return !(dropPositionRelative === 0 && !dropNode.children);
};
- /**
- * The render function.
- */
- render() {
- const {
- className,
- layerGroup,
- map,
- nodeTitleRenderer,
- toggleOnClick,
- ...passThroughProps
- } = this.props;
-
- let ddListeners: any;
- if (passThroughProps.draggable) {
- ddListeners = {
- onDrop: this.onDrop.bind(this)
- };
- }
-
- const finalClassName = className
- ? `${className} ${this.className}`
- : this.className;
-
- return (
-
- {this.state.treeNodes}
-
- );
- }
-}
+ const finalClassName = className
+ ? `${className} ${defaultClassName}`
+ : defaultClassName;
+
+ return (
+
+ );
+};
export default LayerTree;
diff --git a/src/LayerTree/LayerTreeNode/LayerTreeNode.less b/src/LayerTree/LayerTreeNode/LayerTreeNode.less
deleted file mode 100644
index 95308a84f9..0000000000
--- a/src/LayerTree/LayerTreeNode/LayerTreeNode.less
+++ /dev/null
@@ -1,13 +0,0 @@
-.react-geo-layertree-node {
- transition: opacity 1s;
-
- &.out-off-range {
- opacity: 0.5;
- }
-}
-
-.react-geo-layertree {
- .ant-tree-treenode {
- width: 100%;
- }
-}
diff --git a/src/LayerTree/LayerTreeNode/LayerTreeNode.spec.tsx b/src/LayerTree/LayerTreeNode/LayerTreeNode.spec.tsx
deleted file mode 100644
index f3c464ef56..0000000000
--- a/src/LayerTree/LayerTreeNode/LayerTreeNode.spec.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { mount } from 'enzyme';
-import Tree from 'rc-tree';
-import * as React from 'react';
-
-import LayerTreeNode from './LayerTreeNode';
-
-describe('', () => {
-
- const defaultProps = {
- inResolutionRange: true,
- filterTreeNode: jest.fn()
- };
-
- it('is defined', () => {
- expect(LayerTreeNode).not.toBeUndefined();
- });
-
- it('can be rendered (inside a Tree component)', () => {
- const Cmp = (
-
-
-
- );
-
- const wrapper = mount(Cmp);
- expect(wrapper).not.toBeUndefined();
- });
-});
diff --git a/src/LayerTree/LayerTreeNode/LayerTreeNode.tsx b/src/LayerTree/LayerTreeNode/LayerTreeNode.tsx
deleted file mode 100644
index 45ac10faa3..0000000000
--- a/src/LayerTree/LayerTreeNode/LayerTreeNode.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Tree } from 'antd';
-import * as React from 'react';
-const TreeNode = Tree.TreeNode;
-
-import './LayerTreeNode.less';
-
-import { AntTreeNodeProps } from 'antd/lib/tree';
-
-import { CSS_PREFIX } from '../../constants';
-
-export interface BaseProps {
- inResolutionRange?: boolean;
-}
-
-export type LayerTreeNodeProps = BaseProps & AntTreeNodeProps;
-
-/**
- * Class representing a layer tree node
- */
-class LayerTreeNode extends React.PureComponent {
-
- static isTreeNode: number;
-
- /**
- * The render function.
- *
- * @return The element.
- */
- render() {
- const {
- inResolutionRange,
- children,
- icon,
- ...passThroughProps
- } = this.props;
-
- const isFolder = Array.isArray(children) && children.length > 0;
- let addClassName = (inResolutionRange ? 'within' : 'out-off') + '-range';
- addClassName += isFolder ? ' tree-folder' : ' tree-leaf';
- const finalClassname = `${CSS_PREFIX}layertree-node ${addClassName}`;
-
- return (
-
- {children}
-
- );
- }
-
-}
-
-// Otherwise rc-tree wouldn't recognize this component as TreeNode, see
-// https://github.com/react-component/tree/blob/master/src/TreeNode.jsx#L543
-LayerTreeNode.isTreeNode = 1;
-
-export default LayerTreeNode;
diff --git a/src/index.ts b/src/index.ts
index 3941212fd9..b55de81fca 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -26,7 +26,6 @@ import FeatureGrid from './Grid/FeatureGrid/FeatureGrid';
import PropertyGrid from './Grid/PropertyGrid/PropertyGrid';
import LayerSwitcher from './LayerSwitcher/LayerSwitcher';
import LayerTree from './LayerTree/LayerTree';
-import LayerTreeNode from './LayerTree/LayerTreeNode/LayerTreeNode';
import Legend from './Legend/Legend';
import SearchResultsPanel from './Panel/SearchResultsPanel/SearchResultsPanel';
import TimeLayerSliderPanel from './Panel/TimeLayerSliderPanel/TimeLayerSliderPanel';
@@ -51,7 +50,6 @@ export {
LayerSwitcher,
LayerTransparencySlider,
LayerTree,
- LayerTreeNode,
Legend,
MeasureButton,
ModifyButton,
diff --git a/styleguide.config.js b/styleguide.config.js
index e3cc6fc310..ab7ce89a3d 100644
--- a/styleguide.config.js
+++ b/styleguide.config.js
@@ -90,9 +90,6 @@ module.exports = {
}, {
name: 'LayerTree',
components: 'src/LayerTree/**/*.tsx'
- }, {
- name: 'LayerTreeNode',
- components: 'src/LayerTreeNode/**/*.tsx'
}, {
name: 'Legend',
components: 'src/Legend/**/*.tsx'