Skip to content

Commit

Permalink
Feature/layerconfig store (#171)
Browse files Browse the repository at this point in the history
* add layerconfig.store.ts

* add layerconfig redux store & useLayerConfig hook

* working on story using redux for layerConfig

* implements flatLayersConfig with mapConfig as separate AppState Object

* fix layers type

* update store

* flat redux store

* flat layerconfig WIP

* Flat Redux Layerstore WIP

* working sortable list of layers

* renaming store Appstates

* render geojson layers on map

* update layers on map

* rearrange layers in map if order changes

* reorder layers with maplibre moveLayer function

* use LayerTreeListItem to display layer on layertree

* Move GetLayerById Function to store

* hide geojson layer if unchecked

* move layertree with redux stuff to separate folder

* get MapConfig from props

* use visibility from layer for checkbox

* two layertrees with heading

* working example with vectorTiles. But just with one layer in vectortile layers array

* Layers in Folder WIP

* update mapConfig typescript type - adjust MapConfig["layers"] to be flat

* rename LayerOrderList to LayerTree

* add configurable flag and LayerPropertyForm to LayerTreeListItem

* LayerPropertyForm fixes - WIP

* fix LayerPropertyForm

* fix typescript/eslint errors

* set masterVisible to folder if (un)checked and its children

* hide layers on map in folder, if folder is unchecked

* fix layertree story after merging main

* user markerLayer for layerorder

* move LayerOrder in LayerTree

* move LayerOrder on Map

* vectorLayer Example WIP

* Vectortile Layers WIP

* vectorLayer Example WIP

* fix checkbox for vectorlayers

* WIP: masterVisible for nested Vectorlayer

* fix masterVisible for vt layer

* upgrade from config.layout to config.options.layout for Geojsonlayer

* support for wms layer

* fix layerOrder on map

* remove old unused store

* change mapStore.layers from object to array to get rid of redundant layer uuids

* update MlImageMarkerLayer

* fix build

* Update ColorPicker.tsx - remove debug console.log

* fixed a bug: layer array turned into object, when setting masterVisibility

---------

Co-authored-by: Max Tobias Weber <maxtobiasweber@gmail.com>
Co-authored-by: Max Tobias Weber <tobias.weber@wheregroup.com>
  • Loading branch information
3 people authored Oct 1, 2024
1 parent 692bb17 commit 90311cf
Show file tree
Hide file tree
Showing 13 changed files with 1,552 additions and 5 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@tmcw/togeojson": "^5.8.1",
"@reduxjs/toolkit": "^2.0.1",
"@turf/turf": "^6.5.0",
"@types/d3": "^7.4.3",
"@types/react-color": "^3.0.11",
Expand All @@ -45,6 +46,9 @@
"react-color": "^2.19.3",
"react-moveable": "^0.56.0",
"three": "^0.161.0",
"react-redux": "^9.1.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"topojson-client": "^3.1.0",
"uuid": "^9.0.1",
"wms-capabilities": "^0.6.0"
Expand Down
6 changes: 5 additions & 1 deletion src/components/MlVectorTileLayer/MlVectorTileLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import useMap from '../../hooks/useMap';
import { LayerSpecification } from 'maplibre-gl';
import { VectorSourceSpecification } from 'maplibre-gl';

export type ExtendedLayerSpecification = LayerSpecification & {
masterVisible?: boolean;
};

export type MlVectorTileLayerProps = {
mapId?: string;
insertBeforeLayer?: string;
layerId?: string;
sourceOptions?: VectorSourceSpecification;
url?: string;
layers: LayerSpecification[];
layers: ExtendedLayerSpecification[];
};

/**
Expand Down
50 changes: 50 additions & 0 deletions src/decorators/MapContextReduxStoreDecorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useMemo, ReactElement, FC } from 'react';

import { MapComponentsProvider } from '../index';
import MapLibreMap, { MapLibreMapProps } from '../components/MapLibreMap/MapLibreMap';
import './style.css';
import MlNavigationTools from '../components/MlNavigationTools/MlNavigationTools';
import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles';
import getTheme from '../ui_components/MapcomponentsTheme';
import { Decorator } from '@storybook/react';
import store from '../stores/map.store';
import { Provider as ReduxStoreProvider } from 'react-redux';

interface StoryContext {
globals: {
theme?: 'dark' | 'light';
};
}

const makeMapContextDecorators = (options: MapLibreMapProps['options']): Decorator[] => {
return [
(Story: FC, context?: StoryContext): ReactElement => {
const theme = useMemo(() => getTheme(context?.globals?.theme), [context?.globals?.theme]);

return (
<div className="fullscreen_map">
<MapComponentsProvider>
<ReduxStoreProvider store={store}>
<MUIThemeProvider theme={theme}>
<Story />
<MapLibreMap
options={{
zoom: 12.5,
style: 'https://wms.wheregroup.com/tileserver/style/osm-bright.json',
center: [7.0851268, 50.73884],
...(options ? { ...options } : {}),
}}
mapId="map_1"
/>
<MlNavigationTools showZoomButtons={false} mapId="map_1" />
</MUIThemeProvider>
</ReduxStoreProvider>
</MapComponentsProvider>
</div>
);
},
];
};

export default makeMapContextDecorators({});
export { makeMapContextDecorators };
272 changes: 272 additions & 0 deletions src/stores/map.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { MlGeoJsonLayerProps } from 'src/components/MlGeoJsonLayer/MlGeoJsonLayer';
import { MlVectorTileLayerProps } from 'src/components/MlVectorTileLayer/MlVectorTileLayer';
import { Layer } from 'wms-capabilities';
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { MlWmsLayerProps } from '../components/MlWmsLayer/MlWmsLayer';

export interface wmsLoaderConfigProps {
getFeatureInfoUrl: string;
layers: Layer[];
name: string;
open: boolean;
visible: boolean;
wmsUrl: string;
}

export interface wmsConfig {
featureInfoActive?: boolean;
config?: wmsLoaderConfigProps;
url: string;
}

export type WmsLayerConfig = {
type: 'wms';
uuid: string;
name?: string;
id?: string;
config?: MlWmsLayerProps;
masterVisible?: boolean;
};

export type GeojsonLayerConfig = {
type: 'geojson';
uuid: string;
name?: string;
id?: string;
config: MlGeoJsonLayerProps;
masterVisible?: boolean;
configurable?: boolean;
};

export type VtLayerConfig = {
type: 'vt';
uuid: string;
name?: string;
id?: string;
config: MlVectorTileLayerProps;
visible?: boolean;
masterVisible?: boolean;
};

export type FolderLayerConfig = {
type: 'folder';
uuid: string;
name?: string;
visible?: boolean;
masterVisible?: boolean;
id?: string;
config?: undefined;
};

export type LayerConfig = WmsLayerConfig | GeojsonLayerConfig | VtLayerConfig | FolderLayerConfig;

interface MapProps {
center: [number, number];
zoom: number;
}

export interface LayerOrderItem {
uuid: string;
layers?: LayerOrderItem[];
}

interface MapConfig {
/*uuid: string;*/
name: string;
mapProps: MapProps;
layers: LayerConfig[];
layerOrder: LayerOrderItem[];
}

export type MapState = {
mapConfigs: { [key: string]: MapConfig };
};

export type RootState = {
mapConfig: MapState;
};

function processLayerOrderItems(
action: (item: LayerOrderItem, parent?: LayerOrderItem) => void,
items: LayerOrderItem[],
parent?: LayerOrderItem
): void {
items.forEach((item) => {
action(item, parent);
if (item.layers && item.layers.length > 0) {
processLayerOrderItems(action, item.layers, item);
}
});
}

export const initialState: MapState = {
mapConfigs: {},
};
//@ts-ignore
const mapConfigSlice = createSlice({
name: 'mapConfig',
initialState,
reducers: {
// Add or update a MapConfig
setMapConfig: (state, action: PayloadAction<{ key: string; mapConfig: MapConfig }>) => {
const mapConfig = action.payload.mapConfig;
const key = action.payload.key;
//@ts-ignore
state.mapConfigs[key] = mapConfig;
},
// Remove a MapConfig by its uuid
removeMapConfig: (state: MapState, action: PayloadAction<{ key: string }>) => {
delete state.mapConfigs[action.payload.key];
},
// Add or update a layer within a MapConfig
setLayerInMapConfig: (
state: MapState,
action: PayloadAction<{
mapConfigKey: string;
layer: LayerConfig;
}>
) => {
const { mapConfigKey, layer: updatedLayer } = action.payload;
const mapConfig = state.mapConfigs[mapConfigKey];
if (mapConfig) {
for (let i = 0; i < mapConfig.layers.length; i++) {
if (mapConfig.layers[i].uuid === updatedLayer.uuid) {
mapConfig.layers[i] = updatedLayer;
break;
}
}
}
},
// Remove a layer from a MapConfig
removeLayerFromMapConfig: (
state: MapState,
action: PayloadAction<{
mapConfigKey: string;
layerUuid: string;
}>
) => {
const { mapConfigKey, layerUuid } = action.payload;
const mapConfig = state.mapConfigs[mapConfigKey];

if (mapConfig) {
const targetLayerIndex = mapConfig.layers.findIndex((el) => el.uuid === layerUuid);
if (targetLayerIndex !== -1) {
delete mapConfig.layers[targetLayerIndex];
processLayerOrderItems(function (_, parent?: LayerOrderItem): void {
if (parent && parent.layers) {
parent.layers = parent.layers.filter((child) => child.uuid !== layerUuid);
}
}, mapConfig.layerOrder);
}
}
},
updateLayerOrder: (
state: MapState,
action: PayloadAction<{ mapConfigKey: string; newOrder: LayerOrderItem[] }>
) => {
const { mapConfigKey, newOrder } = action.payload;
const mapConfig = state.mapConfigs[mapConfigKey];
if (mapConfig) {
mapConfig.layerOrder = newOrder;
}
},
// masterVisible property will be applied to all children of a folder that is set to be not visible
// masterVisible will over rule the actual layer config if set to false
// if masterVisible is true the actual layerConfig visibility setting is respected
setMasterVisible(
state: MapState,
action: PayloadAction<{ mapConfigKey: string; layerId: string; masterVisible: boolean }>
) {
const { mapConfigKey, layerId, masterVisible } = action.payload;
const mapConfig = state.mapConfigs[mapConfigKey];
if (mapConfig) {
const targetLayerIndex = mapConfig.layers.findIndex((el) => el.uuid === layerId);
if (targetLayerIndex !== -1) {
const layerConfig = mapConfig.layers[targetLayerIndex];
if (layerConfig) {
const updatedLayers = [...mapConfig.layers];
if (layerConfig.type === 'folder') {
mapConfig.layerOrder.forEach((folder) => {
if (folder.uuid === layerId) {
folder.layers?.forEach((childUuid) => {
const childLayerIndex = mapConfig.layers.findIndex(
(el) => el.uuid === childUuid.uuid
);

const childLayer = updatedLayers[childLayerIndex];

updatedLayers[childLayerIndex] = {
...childLayer,
masterVisible,
};
if (childLayer?.type === 'vt' && childLayer?.config?.layers) {
childLayer.config.layers = childLayer.config.layers.map((layer) => ({
...layer,
masterVisible,
}));
}
});
}
});
}
if (layerConfig.type === 'vt' && layerConfig?.config?.layers) {
layerConfig.config.layers = layerConfig.config.layers.map((layer) => ({
...layer,
masterVisible,
}));
}
state.mapConfigs[mapConfigKey].layers = updatedLayers;
}
}
}
},
},
});
export const getLayerByUuid = (state: MapState, uuid: string): LayerConfig | null => {
const mapConfigs = state.mapConfigs;

for (const key in mapConfigs) {
const mapConfig = mapConfigs[key];
const targetLayerIndex = mapConfig.layers.findIndex((el) => el.uuid === uuid);
const foundLayer = mapConfig.layers[targetLayerIndex];
if (foundLayer) return foundLayer;
}
return null;
};

export const extractUuidsFromLayerOrder = (state: RootState, mapConfigKey: string): string[] => {
const mapConfig = state.mapConfig.mapConfigs[mapConfigKey];
if (!mapConfig) {
return [];
}
const layerOrder = mapConfig.layerOrder;
const uuids: string[] = [];
function extractUuids(items: LayerOrderItem[]): void {
items.forEach((item) => {
uuids.push(item.uuid);
if (item.layers && item.layers.length > 0) {
extractUuids(item.layers);
}
});
}

extractUuids(layerOrder);
return uuids;
};

const store = configureStore({
reducer: {
mapConfig: mapConfigSlice.reducer,
},
});

export const {
setMapConfig,
removeMapConfig,
setLayerInMapConfig,
removeLayerFromMapConfig,
updateLayerOrder,
setMasterVisible,
} = mapConfigSlice.actions;
export default store;
5 changes: 1 addition & 4 deletions src/ui_components/ColorPicker/ColorPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import { converters } from './transformers';
export interface ColorPickerProps {
onChange?: (value: string) => void;
convert: 'rgb' | 'rgba' | 'rgba_hex' | 'hex' | 'rgba_rgb';
internalValue?: string;
setValue?: (value: string) => void;
value?: string;
}

const ColorPicker = ({ convert, ...props }: ColorPickerProps) => {
const [showPicker, setShowPicker] = useState(false);
const [value, setValue] = useState(props?.value || '');
const value = props?.value || '';

return (
<>
Expand Down Expand Up @@ -58,7 +56,6 @@ const ColorPicker = ({ convert, ...props }: ColorPickerProps) => {
color={value}
onChange={(c) => {
const newValue = converters[convert](c);
setValue(newValue);
props?.onChange?.(newValue);
}}
/>
Expand Down
Loading

0 comments on commit 90311cf

Please sign in to comment.