Skip to main content

第二节:创建图层菜单,定义图层管理器

上一节完成后,我们已经创建了地图,根组件,并且在根组件中渲染了测试的图层菜单以及工具按钮,这一节我们将创建一个完整的图层菜单,并且对如何创建、删除图层做一些思考,最终设计一个用于管理这个项目图层的图层管理器。

创建图层菜单

接下来,我们先把图层菜单构建出来,我们只需要对我们需要实现的图层进行分析,即可通过配置文件配置出菜单。 从上一节的菜单数据源的定义可以看出,我们只要一个IMenuItem数组就可以,目前可以明确的是有气象实况和GFS预报两部分,因此我们分别进行创建。

在哪里创建配置信息

最简单的方式就是直接在MenuList的属性赋值的时候直接传进去,这样的好处是简单容易理解,缺点是这个组件的UI和业务逻辑完全关联,我们先使用这种方式创建,直接在组件里面定义一个实况配置项和一个预报配置项,然后在MenuList创建的地方作为属性值传进去。

气象实况菜单
const realTimeConfig = {
        name: "气象实况",
        childs: [
            {
                name: "Metar报",
                id: "realtime_metar",
            },
            {
                name: "卫星云图",
                id: "realtime_sate",
            }
        ],
        id: "realtime"
    }

然后将MenuList的组件创建代码修改如下:

<MenuList
    dataSource={[realTimeConfig]}
    selectedItemIds={["test"]}
    mode="inline"
    theme="dark"
/>

上面我们将原来的测试数据源改成了我们刚创建的实时信息数据源,于是我们的拥有了实况菜单: 第二节-气象实况菜单.jpg

GFS预报菜单
const gfsConfig = {
    name: "GFS预报",
    childs: [
        {
            name: "2米温度-024",
            id: "gfs_temp2m_024"
        },
        {
            name: "累计降水-024",
            id: "gfs_accprecp_024"
        },
        {
            name: "850hPa风场-024",
            id: "gfs_850uv_024"
        },
        {
            name: "2米相对湿度-024",
            id: "gfs_rhu2m-024"
        },
        {
            name: "零度层相对湿度-024",
            id: "gfs_0rhu_024"
        },
        {
            name: "地面气压-024",
            id: "gfs_pslp_024"
        }
    ],
    id: "gfs"
}

这样只需要在MenuList构造的时候加入gfs的配置即可:

<MenuList
    dataSource={[realTimeConfig,gfsConfig]}
    selectedItemIds={["test"]}
    mode="inline"
    theme="dark"
// defaultOpenKeys={props.layerStore.menuData.map(g => g.id)}
/>

设置默认展开

这里默认菜单是关闭的,点击后可以打开,这与我们通常的业务需要不太吻合,我们希望有些菜单是默认打开的,本示例中我们把所有菜单都默认打开,这就需要设置defaultOpenKeys属性,这个属性是antd的菜单自带属性,我们只需要做一个遍历就可以把所有菜单都打开,在这之前,我们先把这两个菜单合并成一个配置对象menuConfig:

const menuConfig=[
	//分别把realtime和gfs的配置写到这里
]

接下来设置默认打开

<MenuList
    dataSource={menuConfig}
    selectedItemIds={["test"]}
    mode="inline"
    theme="dark"
    defaultOpenKeys={menuConfig.map(g => g.id)}
/>

第二节-菜单.jpg

响应菜单事件

要让图层能在地图显示和删除,就需要响应图层菜单的选中和取消选中事件,MenuList中已经实现了相关的回调属性 onItemSelectedonItemDeSelect,因此创建function来进行响应:

...
async function selectItem(){

}

function deSelectItem() {

}

return (
 ...
            <MenuList
                dataSource={menuConfig}
                selectedItemIds={["test"]}
                mode="inline"
                theme="dark"
                defaultOpenKeys={menuConfig.map(g => g.id)}
                onItemSelected={selectItem}
				onItemDeSelect={deSelectItem}
...
)

可以看到,选中事件的响应函数是异步的,因为后续要加载图层,获取数据是异步操作。

设定当前选中状态

默认我们还是选中的test菜单,但是当前并没有test菜单,我们就需要一个状态来表示当前选中的所有菜单项,因此使用useState来进行状态处理:

const defaults: IMenuItem[] = [];
const [selected, setSelected] = useState(defaults);
...
<MenuList
...
	selectedItemIds={selected.map(i => i.id)}
...
/>
...

可以看到,选中菜单是一个数组,数组里面的对象是图层配置的菜单项,这样可以在后面获取当前菜单的各种信息时更加方便。

接下来,就需要在选中和取消选中的事件中对选中的项进行状态设定,如果是单选模式,那就非常简单,只要在选中的时候 setSelected([item]),在取消选中的时候setSelected([])即可。

菜单支持多选

但是咱们目前计划支持单选和多选,首先我们需要指定菜单支持多选,这只要在MenuList创建的时候设置即可:

<MenuList
	...
    multiple={true}
/>
图层叠加与不叠加

为了实现图层叠加和不叠加,就需要在图层的配置项中具有单选和多选的信息,可以通过IMenuItem的userData对象来设定这个信息,userData是用户自定义信息,是一个对象,在这里,我们在userData对象中增加一个overlay属性,属性值是boolean类型,这样未设置或者设置为false表示不支持多层叠加,如果希望支持,只要把这个值设置为true就行,目前,气象实况的站点实况希望支持的,其配置修改如下:

{
    name: "Metar报",
    id: "realtime_metar",
    userData: {
        overlay: true
    }
},

有了这样的配置后,就可以在选中和取消选中的中考虑单选和多选的逻辑,这部分代码如下所示:

 async function selectItem(item: IMenuItem) {
    let items = [];
    if (selected.length === 0) {
        items = [item];
    } else if (item.userData?.overlay) {
        items = [...selected, item]
    } else {
        //还没有不可叠加的图层加入
        if (selected[0].userData?.overlay) {
            items = [item, ...selected];
        } else {
            //第一个图层是不可叠加图层,要去掉
            items = [...selected];
            //todo:remove layer

            items[0] = item;
        }
    }
    setSelected(items);
    //todo add layer   
}

function deSelectItem(item: IMenuItem) {
    const itemIdx = selected.findIndex(i => i.id === item.id);
    if (itemIdx < 0) {
        message.error("当前选中的节点有误!");
        return;
    }
    const items = [...selected];
    items.splice(itemIdx, 1);
    setSelected(items);
    //todo remove layer
}
以上叠加与不叠加的逻辑主要是将不支持叠加的图层设定为items中的第一个,后续的都是支持叠加的,这样如果当前菜单支持叠加,支持添加到最后;如果不支持,则当前先去掉当前不支持叠加的,然后把当前的加到第一个;当然当前如果没有任何菜单,则不需要考虑叠加逻辑,直接加进去即可。

到目前为止,我们已经可以在菜单上实现点击后选中和取消的效果了,接下来就可以开始跟数据及图层进行关联了。我们在两个响应方法中留下来的todo就是做这个事情。

图层管理器

最简单的图层关联方式,就是在上面的selectItem和deSelectItem中预留的todo地方进行图层的创建和删除,但是咱们需要显示的图层类型较多,全部都放到这个UI组件中会显得很臃肿,而且扩展性也不好,因此我们把图层相关的内容抽取到组件外部,然后作为组件的属性传入,外部的这些图层相关的属性和方法我们使用一个类来进行封装,在这里,我们称为LayerStore,说明这是一个对图层进行操作的类。

设计LayerStore功能

LayerStore需要至少具备以下功能:

  • 根据当前选择的IMenuItem添加图层
  • 根据当前取消选择的IMenuItem删除图层
  • 将原来直接写在组件里面的图层菜单移到该类

因此,该类的结构如下:

class LayerStore {

    public menuData: IMenuItem[] = [
        //原来UI组件中的menuConfig内容
    ];

    public async createLayer(item: IMenuItem): Promise<boolean> {
        return true;
    }

    public removeLayer(item: IMenuItem) {
    }
}

这里的menuData就是UI组件中的menuConfig,现在可以拷贝过去了。

同时,对原来的组件进行微调,设计一个props的类型,然后将以上图层管理类的实例通过props传入,并且把menuData绑定到MenuList的dataSource后删除组件中的menuConfig:

interface IAppProps {
    layerStore: LayerStore
}
...
const AviationApp: React.FC<IAppProps> = (props) => {
	...
    return (
    			<MenuList
                    dataSource={props.layerStore.menuData}
                    ...
                    defaultOpenKeys={props.layerstore.menuData.map(g => g.id)}
                    ...
                />
    )
    ...
}
...
const load = async () => {
	...
	const layerStore = new LayerStore();
    ReactDOM.render(<AviationApp
        layerStore={layerStore}
    />, document.getElementById("plugins"));
}
...

绑定图层创建和删除方法

图层管理器中已经有了图层的添加和删除方法,我们将其绑定到UI组件中:

async function selectItem(item: IMenuItem) {
    let items = [];
    if (selected.length === 0) {
        items = [item];
    } else if (item.userData?.overlay) {
        items = [...selected, item]
    } else {
        //还没有不可叠加的图层加入
        if (selected[0].userData?.overlay) {
            items = [item, ...selected];
        } else {
            //第一个图层是不可叠加图层,要去掉
            items = [...selected];
            props.layerStore.removeLayer(selected[0]);
            items[0] = item;
        }
    }
    setSelected(items);
    props.layerStore.createLayer(item);   
}

function deSelectItem(item: IMenuItem) {
    ...
    setSelected(items);
    props.layerStore.removeLayer(item);
}

这样,我们就完成了图层管理器的基础结构了,接下来要做的就是真正的创建和删除图层了。

默认选中菜单

如果我们希望有些图层一开始就有,那就需要在组件初始化完成后,就自动选中这些菜单,因此,我们需要在组件加载完成的状态中进行setSelected操作,而默认选中的项,我们同样可以放到LayerStore中:

public defaultSelectedItemIds = ["realtime_metar"];

实现这个需要使用useEffect,只要在组件加载的时候,对当前选中的菜单进行选择即可。

由于React的状态设置是异步的,而且每次设置之后都会重新执行函数组件,因此如果直接使用循环来设置选中的状态,当有多个选中的时候,会造成当前状态和最新状态不一致,所以我们使用一次性设置状态,然后分多次加载图层,为了让seleceItem方法支持不更新状态,添加一个update参数,默认为true,只有在批量更新状态的时候才传入flase。
useEffect(() => {
    //初始化逻辑
    if (props.layerStore.defaultSelectedItemIds) {
        const selectedItems: IMenuItem[] = [];
        const getSelectedItems = (items: IMenuItem[]) => {
            items.forEach(i => {
                if (i.childs?.length) {
                    getSelectedItems(i.childs);
                } else if (props.layerStore.defaultSelectedItemIds.indexOf(i.id) >= 0) {
                    selectedItems.push(i);
                }
            });
        };
        getSelectedItems(props.layerStore.menuData);
        selectItems(selectedItems);
    }
}, []);

async function selectItems(items: IMenuItem[]) {
    setSelected(items);
    for (const item of items) {
        await selectItem(item, false);
    }
}

async function selectItem(item: IMenuItem, update = true) {
    if (update) {
        ...
    }
    props.layerStore.createLayer(item);   
}

现在,可以前往下一节开始真正接入数据了!

完整代码和效果