Skip to content

Commit

Permalink
Integrate layerList to store and add breadcrumbs (#80)
Browse files Browse the repository at this point in the history
* Integrate layerList to store and add breadcrumbs

Signed-off-by: Junqiu Lei <[email protected]>
  • Loading branch information
junqiu-lei authored Nov 10, 2022
1 parent 61933bb commit 1d768bd
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 179 deletions.
7 changes: 6 additions & 1 deletion maps_dashboards/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const PLUGIN_NAME = 'Maps';
export const MAP_VECTOR_TILE_BASIC_STYLE = 'https://tiles.maps.opensearch.org/styles/basic.json';
export const MAP_GLYPHS = 'https://tiles.maps.opensearch.org/fonts/{fontstack}/{range}.pbf';
export const MAP_VECTOR_TILE_DATA_SOURCE = 'https://tiles.maps.opensearch.org/data/v1.json';
export const MAP_DEFAULT_MIN_ZOOM = 0;
export const MAP_DEFAULT_MAX_ZOOM = 22;
export const MAP_DEFAULT_OPACITY = 1;

// Starting position [lng, lat] and zoom
export const MAP_INITIAL_STATE = {
Expand All @@ -18,7 +21,9 @@ export const MAP_INITIAL_STATE = {
};

export const APP_PATH = {
CREATE_MAP: '/create-map',
LANDING_PAGE_PATH: '/',
CREATE_MAP: '/create',
EDIT_MAP: '/:id',
};

export enum DASHBOARDS_MAPS_LAYER_TYPE {
Expand Down
4 changes: 2 additions & 2 deletions maps_dashboards/public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const MapsDashboardsApp = () => {
<I18nProvider>
<div>
<Switch>
<Route path={APP_PATH.CREATE_MAP} render={() => <MapPage />} />
<Route exact path="/" render={() => <MapsList />} />
<Route path={[APP_PATH.CREATE_MAP, APP_PATH.EDIT_MAP]} render={() => <MapPage />} />
<Route exact path={APP_PATH.LANDING_PAGE_PATH} render={() => <MapsList />} />
</Switch>
</div>
</I18nProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect, useState } from 'react';
import React, { memo, useEffect, useState } from 'react';
import {
EuiPanel,
EuiTitle,
Expand All @@ -25,6 +25,9 @@ import {
DASHBOARDS_MAPS_LAYER_TYPE,
LAYER_VISIBILITY,
MAP_VECTOR_TILE_BASIC_STYLE,
MAP_DEFAULT_OPACITY,
MAP_DEFAULT_MAX_ZOOM,
MAP_DEFAULT_MIN_ZOOM,
} from '../../../common';
import { layersFunctionMap } from '../../model/layersFunctions';

Expand All @@ -34,46 +37,49 @@ interface MaplibreRef {

interface Props {
maplibreRef: MaplibreRef;
mapIdFromUrl: string;
setLayers: (layers: ILayerConfig[]) => void;
layers: ILayerConfig[];
}

const LayerControlPanel = ({ maplibreRef }: Props) => {
const LayerControlPanel = memo(({ maplibreRef, mapIdFromUrl, setLayers, layers }: Props) => {
const [isLayerConfigVisible, setIsLayerConfigVisible] = useState(false);
const [isLayerControlVisible, setIsLayerControlVisible] = useState(true);
const [selectedLayerConfig, setSelectedLayerConfig] = useState<ILayerConfig | undefined>();

const [selectedLayerConfig, setSelectedLayerConfig] = useState<ILayerConfig>({
iconType: '',
name: '',
type: '',
id: '',
zoomRange: [],
opacity: 1,
visibility: '',
});

const initialLoadLayer: ILayerConfig = {
const initialDefaultLayer: ILayerConfig = {
iconType: 'visMapRegion',
id: uuidv4(),
type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP,
name: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP,
zoomRange: [0, 22],
opacity: 1,
zoomRange: [MAP_DEFAULT_MIN_ZOOM, MAP_DEFAULT_MAX_ZOOM],
opacity: MAP_DEFAULT_OPACITY,
visibility: LAYER_VISIBILITY.VISIBLE,
layerSpec: {
OSMUrl: MAP_VECTOR_TILE_BASIC_STYLE,
},
};

const [layers, setLayers] = useState<ILayerConfig[]>([initialLoadLayer]);

// Initially load the layers from the saved object
useEffect(() => {
maplibreRef.current?.on('load', function () {
if (layers && mapIdFromUrl) {
layers.forEach((layer) => {
layersFunctionMap[layer.type]?.initial(maplibreRef, layer);
layersFunctionMap[layer.type]?.initialize(maplibreRef, layer);
});
} else {
maplibreRef.current?.on('load', function () {
if (!mapIdFromUrl) {
layersFunctionMap[initialDefaultLayer.type]?.initialize(maplibreRef, initialDefaultLayer);
setLayers([initialDefaultLayer]);
}
});
});
}, []);
}
}, [layers]);

const updateLayer = () => {
if (!selectedLayerConfig) {
return;
}
const layersClone = [...layers];
const index = layersClone.findIndex((layer) => layer.id === selectedLayerConfig.id);
if (index <= -1) {
Expand All @@ -86,9 +92,7 @@ const LayerControlPanel = ({ maplibreRef }: Props) => {
};
}
setLayers(layersClone);
setTimeout(function () {
layersFunctionMap[selectedLayerConfig.type]?.update(maplibreRef, selectedLayerConfig);
}, 50);
layersFunctionMap[selectedLayerConfig.type]?.update(maplibreRef, selectedLayerConfig);
};

const removeLayer = (index: number) => {
Expand Down Expand Up @@ -149,7 +153,12 @@ const LayerControlPanel = ({ maplibreRef }: Props) => {
aria-label="layer in the map layers list"
isDisabled={isDisabled}
onClick={() => {
if (selectedLayerConfig.id === layer.id && !isLayerConfigVisible) {
setSelectedLayerConfig(layer);
if (
selectedLayerConfig &&
selectedLayerConfig.id === layer.id &&
!isLayerConfigVisible
) {
setIsLayerConfigVisible(true);
}
}}
Expand All @@ -158,7 +167,7 @@ const LayerControlPanel = ({ maplibreRef }: Props) => {
<EuiFlexGroup justifyContent="flexEnd" gutterSize="none">
<EuiFlexItem grow={false} className="layerControlPanel__layerFunctionButton">
<EuiButtonEmpty
iconType='eyeClosed'
iconType="eyeClosed"
size="s"
onClick={() => {
if (layer.visibility === LAYER_VISIBILITY.VISIBLE) {
Expand Down Expand Up @@ -192,7 +201,7 @@ const LayerControlPanel = ({ maplibreRef }: Props) => {
</div>
);
})}
{isLayerConfigVisible && (
{isLayerConfigVisible && selectedLayerConfig && (
<LayerConfigPanel
setIsLayerConfigVisible={setIsLayerConfigVisible}
selectedLayerConfig={selectedLayerConfig}
Expand Down Expand Up @@ -224,6 +233,6 @@ const LayerControlPanel = ({ maplibreRef }: Props) => {
</EuiButton>
</EuiFlexItem>
);
};
});

export { LayerControlPanel };
45 changes: 26 additions & 19 deletions maps_dashboards/public/components/map_container/map_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@ import { Map as Maplibre, NavigationControl } from 'maplibre-gl';
import { LayerControlPanel } from '../layer_control_panel';
import './map_container.scss';
import { MAP_INITIAL_STATE, MAP_GLYPHS, MAP_VECTOR_TILE_DATA_SOURCE } from '../../../common';
import { ILayerConfig } from '../../model/ILayerConfig';

export const MapContainer = () => {
interface MapContainerProps {
mapIdFromUrl: string;
setLayers: (layers: ILayerConfig[]) => void;
layers: ILayerConfig[];
}

export const MapContainer = ({ mapIdFromUrl, setLayers, layers }: MapContainerProps) => {
const maplibreRef = useRef<Maplibre | null>(null);
const mapContainer = useRef(null);
const [mounted, setMounted] = useState(false);
const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom);
const [zoom, setZoom] = useState<number>(MAP_INITIAL_STATE.zoom);

useEffect(() => {
if (!mapContainer.current) return;
const mbStyle = {
version: 8 as 8,
sources: {},
Expand All @@ -25,41 +33,40 @@ export const MapContainer = () => {
};

maplibreRef.current = new Maplibre({
// @ts-ignore
container: mapContainer.current,
container: mapContainer.current!,
center: [MAP_INITIAL_STATE.lng, MAP_INITIAL_STATE.lat],
zoom,
style: mbStyle,
});

maplibreRef.current.addControl(new NavigationControl({ showCompass: false }), 'top-right');
maplibreRef.current.on('style.load', function () {
// @ts-ignore
maplibreRef.current.addSource('openmaptiles', {
const maplibreInstance = maplibreRef.current!;
maplibreInstance.addControl(new NavigationControl({ showCompass: false }), 'top-right');
maplibreInstance.on('style.load', function () {
maplibreInstance.addSource('openmaptiles', {
type: 'vector',
url: MAP_VECTOR_TILE_DATA_SOURCE,
});
setMounted(true);
});
}, []);

useEffect(() => {
// @ts-ignore
maplibreRef.current.on('move', () => {
// @ts-ignore
return setZoom(maplibreRef.current.getZoom().toFixed(2));
maplibreInstance.on('move', () => {
return setZoom(Number(maplibreInstance.getZoom().toFixed(2)));
});
}, [zoom]);

useEffect(() => {}, []);
}, []);

return (
<div>
<EuiPanel hasShadow={false} hasBorder={false} color="transparent" className="zoombar">
<p> Zoom: {zoom} </p>
</EuiPanel>
<div className="layerControlPanel-container">
{mounted && <LayerControlPanel maplibreRef={maplibreRef} />}
{mounted && (
<LayerControlPanel
maplibreRef={maplibreRef}
mapIdFromUrl={mapIdFromUrl}
layers={layers}
setLayers={setLayers}
/>
)}
</div>
<div className="map-container" ref={mapContainer} />
</div>
Expand Down
31 changes: 28 additions & 3 deletions maps_dashboards/public/components/map_page/map_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,40 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { SimpleSavedObject } from 'opensearch-dashboards/public';
import { MapContainer } from '../map_container';
import { MapTopNavMenu } from '../map_top_nav';
import { ILayerConfig } from '../../model/ILayerConfig';
import { MapServices } from '../../types';
import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public';
import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes';

export const MapPage = () => {
const [layers, setLayers] = useState<ILayerConfig[]>([]);
const { id: mapIdFromUrl } = useParams<{ id: string }>();
const [savedMapObject, setSavedMapObject] = useState<SimpleSavedObject<
MapSavedObjectAttributes
> | null>();
const { services } = useOpenSearchDashboards<MapServices>();
const {
savedObjects: { client: savedObjectsClient },
} = services;

useEffect(() => {
if (mapIdFromUrl) {
savedObjectsClient.get<MapSavedObjectAttributes>('map', mapIdFromUrl).then((res) => {
setSavedMapObject(res);
setLayers(JSON.parse(res.attributes.layerList as string));
});
}
}, [savedObjectsClient, mapIdFromUrl]);

return (
<div>
<MapTopNavMenu />
<MapContainer />
<MapTopNavMenu mapIdFromUrl={mapIdFromUrl} savedMapObject={savedMapObject} layers={layers} />
<MapContainer mapIdFromUrl={mapIdFromUrl} layers={layers} setLayers={setLayers} />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ import {
} from '../../../../../src/plugins/saved_objects/public';
import { MapServices } from '../../types';

export const getTopNavConfig = (services: MapServices) => {
const {
interface GetTopNavConfigParams {
mapIdFromUrl: string;
layers: any;
title: string;
description: string;
setTitle: (title: string) => void;
setDescription: (description: string) => void;
}

export const getTopNavConfig = (
{
notifications: { toasts },
i18n: { Context: I18nContext },
savedObjects: { client: savedObjectsClient },
} = services;

history,
}: MapServices,
{ mapIdFromUrl, layers, title, description, setTitle, setDescription }: GetTopNavConfigParams
) => {
const topNavConfig: TopNavMenuData[] = [
{
iconType: 'save',
Expand All @@ -29,19 +40,36 @@ export const getTopNavConfig = (services: MapServices) => {
defaultMessage: `Save`,
}),
run: (_anchorElement) => {
const onModalSave = async (onSaveProps: OnSaveProps) => {
const savedMap = await savedObjectsClient.create('map', {
title: onSaveProps.newTitle,
description: onSaveProps.newDescription,
// TODO: Integrate other attributes to saved object
});
const id = savedMap.id;
const onModalSave = async ({ newTitle, newDescription }: OnSaveProps) => {
let newlySavedMap;
if (mapIdFromUrl) {
// edit existing map
newlySavedMap = await savedObjectsClient.update('map', mapIdFromUrl, {
title: newTitle,
description: newDescription,
layerList: JSON.stringify(layers),
});
} else {
// save new map
newlySavedMap = await savedObjectsClient.create('map', {
title: newTitle,
description: newDescription,
layerList: JSON.stringify(layers),
});
}
const id = newlySavedMap.id;
if (id) {
history.push({
...history.location,
pathname: `${id}`,
});
setTitle(newTitle);
setDescription(newDescription);
toasts.addSuccess({
title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', {
defaultMessage: `Saved ${onSaveProps.newTitle}`,
defaultMessage: `Saved ${newTitle}`,
values: {
visTitle: savedMap.attributes.title,
visTitle: newTitle,
},
}),
});
Expand All @@ -50,9 +78,10 @@ export const getTopNavConfig = (services: MapServices) => {
};

const documentInfo = {
title: '',
description: '',
title,
description,
};

const saveModal = (
<SavedObjectSaveModalOrigin
documentInfo={documentInfo}
Expand Down
Loading

0 comments on commit 1d768bd

Please sign in to comment.