Skip to content

Commit ea97284

Browse files
lenaghubMartin Alzuetacioddi
authored
Feature/ml globe button (#229)
Co-authored-by: Martin Alzueta <[email protected]> Co-authored-by: Max Tobias Weber <[email protected]>
1 parent 5e1028c commit ea97284

File tree

7 files changed

+244
-68
lines changed

7 files changed

+244
-68
lines changed
401 KB
Loading
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import { mount } from '@cypress/react';
3+
import { expect } from 'chai';
4+
import { composeStories } from '@storybook/testing-react';
5+
import * as stories from './MlGlobeButton.stories';
6+
7+
const { CatalogueDemo }: any = composeStories(stories);
8+
9+
describe('MlGlobeButton', () => {
10+
beforeEach(() => {
11+
// Reset window._map so that each test gets a new, clean map context
12+
cy.window().then((win) => {
13+
delete (win as any)._map;
14+
// Remove icon elements that might persist between tests
15+
const mapIcon = win.document.querySelector('[data-testid="MapIcon"]');
16+
if (mapIcon) mapIcon.remove();
17+
const publicIcon = win.document.querySelector('[data-testid="PublicIcon"]');
18+
if (publicIcon) publicIcon.remove();
19+
});
20+
});
21+
22+
it('shows MapIcon as start state and toggles between MapIcon and PublicIcon', () => {
23+
mount(<CatalogueDemo />);
24+
cy.window().should((win) => expect((win as any)._map).to.exist);
25+
26+
cy.get('[data-testid="MapIcon"]').should('exist');
27+
cy.get('[data-testid="PublicIcon"]').should('not.exist');
28+
cy.get('button').click();
29+
cy.get('[data-testid="PublicIcon"]').should('exist');
30+
cy.get('[data-testid="MapIcon"]').should('not.exist');
31+
cy.get('button').click();
32+
cy.get('[data-testid="MapIcon"]').should('exist');
33+
cy.get('[data-testid="PublicIcon"]').should('not.exist');
34+
});
35+
36+
it('changes the projection on the map instance attached to window', () => {
37+
mount(<CatalogueDemo />);
38+
cy.window().should((win) => expect((win as any)._map).to.exist);
39+
40+
cy.window().should((win) => {
41+
const map = (win as any)._map;
42+
expect(map.getProjection()).to.equal(undefined);
43+
});
44+
45+
cy.get('button').click();
46+
cy.window().should((win) => {
47+
const map = (win as any)._map;
48+
expect(map.getProjection().type).to.eq('globe');
49+
});
50+
51+
cy.get('button').click();
52+
cy.window().should((win) => {
53+
const map = (win as any)._map;
54+
expect(map.getProjection().type).to.eq('mercator');
55+
});
56+
});
57+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "MlGlobeButton",
3+
"title": "Globe Button",
4+
"description": "Displays the map as a globe or as a mercator projection. To see the effect, set a small zoom level.",
5+
"i18n": {
6+
"de": {
7+
"title": "Globus Button",
8+
"description": "Zeigt die Karte als Globus oder als Mercator-Projektion an. Um den Effekt zu sehen, am besten eine niedrige Zoomstufe einstellen."
9+
}
10+
},
11+
"tags": ["Map add-on"],
12+
"category": "add-ons",
13+
"type": "component"
14+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
3+
import MlGlobeButton, { MlGlobeButtonProps } from './MlGlobeButton';
4+
5+
import lowZoomDecorator from '../../decorators/LowZoomDecorator';
6+
7+
const storyoptions = {
8+
title: 'MapComponents/MlGlobeButton',
9+
component: MlGlobeButton,
10+
argTypes: {},
11+
decorators: lowZoomDecorator,
12+
};
13+
export default storyoptions;
14+
15+
const Template = (props: MlGlobeButtonProps) => <MlGlobeButton {...props} />;
16+
17+
export const CatalogueDemo = Template.bind({});
18+
CatalogueDemo.parameters = {};
19+
CatalogueDemo.args = {
20+
style: {
21+
position: 'absolute',
22+
transform: 'scale(5)',
23+
left: '50%',
24+
top: '60%',
25+
},
26+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useState, useEffect, CSSProperties } from 'react';
2+
import useMap from '../../hooks/useMap';
3+
import { Button, styled } from '@mui/material';
4+
import MapIcon from '@mui/icons-material/Map';
5+
import PublicIcon from '@mui/icons-material/Public';
6+
7+
export interface MlGlobeButtonProps {
8+
/**
9+
* Id of the target MapLibre instance in mapContext
10+
*/
11+
mapId?: string;
12+
/**
13+
* Id of an existing layer in the mapLibre instance to help specify the layer order
14+
* This layer will be visually beneath the layer with the "insertBeforeLayer" id.
15+
*/
16+
insertBeforeLayer?: string;
17+
/**
18+
* Style object to adjust css definitions of the component.
19+
*/
20+
style?: CSSProperties;
21+
/**
22+
* Initial projection mode of the map.
23+
*/
24+
mode?: 'globe' | 'mercator';
25+
}
26+
27+
/**
28+
* Projection component that displays the map as a globe or as a mercator projection.
29+
* @component
30+
*/
31+
32+
const GlobeButtonStyled = styled(Button)(({ theme }) => ({
33+
zIndex: 1000,
34+
color: theme.palette.navigation.buttonColor,
35+
transform: 'scale(1)',
36+
}));
37+
38+
const MlGlobeButton = (props: MlGlobeButtonProps) => {
39+
const mapHook = useMap({
40+
mapId: props.mapId,
41+
waitForLayer: props.insertBeforeLayer,
42+
});
43+
44+
const [projection, setProjection] = useState<'globe' | 'mercator'>(props.mode || 'mercator');
45+
46+
useEffect(() => {
47+
const current = mapHook.map?.map.getProjection?.()?.type;
48+
if (current !== projection) {
49+
mapHook.map?.setProjection({ type: projection });
50+
}
51+
}, [mapHook.map]);
52+
53+
const handleClick = () => {
54+
if (!mapHook.map) return;
55+
const next = projection === 'globe' ? 'mercator' : 'globe';
56+
mapHook.map.setProjection({ type: next });
57+
setProjection(next);
58+
};
59+
60+
return (
61+
<>
62+
<GlobeButtonStyled variant="navtools" onClick={handleClick} style={props.style}>
63+
{projection === 'globe' ? (
64+
<PublicIcon data-testid="PublicIcon" sx={{ fontSize: { xs: '1.4em', md: '1em' } }} />
65+
) : (
66+
<MapIcon data-testid="MapIcon" sx={{ fontSize: { xs: '1.4em', md: '1em' } }} />
67+
)}
68+
</GlobeButtonStyled>
69+
</>
70+
);
71+
};
72+
73+
MlGlobeButton.defaultProps = {
74+
mapId: undefined,
75+
mode: 'mercator',
76+
};
77+
export default MlGlobeButton;

src/components/MlNavigationTools/MlNavigationTools.stories.tsx

Lines changed: 48 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { useState } from 'react';
33
import MlNavigationTools, { MlNavigationToolsProps } from './MlNavigationTools';
44
import noNavToolsDecorator from '../../decorators/NoNavToolsDecorator';
@@ -9,6 +9,7 @@ import FormGroup from '@mui/material/FormGroup';
99
import FormControlLabel from '@mui/material/FormControlLabel';
1010
import Sidebar from '../../ui_components/Sidebar';
1111
import TopToolbar from '../../ui_components/TopToolbar';
12+
import useMap from '../../hooks/useMap';
1213

1314
const storyoptions = {
1415
title: 'MapComponents/MlNavigationTools',
@@ -25,34 +26,35 @@ export default storyoptions;
2526
const Template = (props: MlNavigationToolsProps) => <MlNavigationTools {...props} />;
2627

2728
const catalogueTemplate = () => {
29+
const mapHook = useMap();
2830
const [openSidebar, setOpenSidebar] = useState(true);
29-
const [ThreeDButton, setThreeDButton] = useState(false);
30-
const [CenterLocationButton, setCenterLocationButton] = useState(false);
31-
const [ZoomButtons, setZoomButtons] = useState(true);
32-
const [FollowGpsButton, setFollowGpsButton] = useState(false);
33-
const [showCustomButton, setShowCustomButton] = useState<boolean>(false);
31+
const [threeDButton, setThreeDButton] = useState(false);
32+
const [globeButton, setGlobeButton] = useState(false);
33+
const [centerLocationButton, setCenterLocationButton] = useState(false);
34+
const [zoomButtons, setZoomButtons] = useState(true);
35+
const [followGpsButton, setFollowGpsButton] = useState(false);
36+
const [showCustomButton, setShowCustomButton] = useState(false);
3437
const [alternativePosition, setAlternativePosition] = useState(false);
3538

36-
const handleChange1 = () => {
37-
setThreeDButton(!ThreeDButton);
38-
};
39-
const handleChange2 = () => {
40-
setCenterLocationButton(!CenterLocationButton);
41-
};
42-
const handleChange3 = () => {
43-
setZoomButtons(!ZoomButtons);
44-
};
45-
const handleChange4 = () => {
46-
setFollowGpsButton(!FollowGpsButton);
47-
};
48-
49-
const handleCustomButtonChange = (event: React.ChangeEvent<HTMLInputElement>) => {
50-
setShowCustomButton(event.target.checked);
51-
};
52-
53-
const handleAlternativePositionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
54-
setAlternativePosition(event.target.checked);
55-
};
39+
const tools = [
40+
{ text: 'Alternative Position', const: alternativePosition, setter: setAlternativePosition },
41+
{ text: 'Show 2D/3D Button', const: threeDButton, setter: setThreeDButton },
42+
{ text: 'Show Globe Button', const: globeButton, setter: setGlobeButton },
43+
{
44+
text: 'Show CenterLocation Button',
45+
const: centerLocationButton,
46+
setter: setCenterLocationButton,
47+
},
48+
{ text: 'Show Zoom Buttons', const: zoomButtons, setter: setZoomButtons },
49+
{ text: 'Show FollowGPS Button', const: followGpsButton, setter: setFollowGpsButton },
50+
{ text: 'Add a custom Button', const: showCustomButton, setter: setShowCustomButton },
51+
];
52+
53+
useEffect(() => {
54+
if (globeButton && mapHook.map) {
55+
mapHook.map.easeTo({ zoom: 2 });
56+
}
57+
}, [globeButton, mapHook.map]);
5658

5759
return (
5860
<>
@@ -71,40 +73,24 @@ const catalogueTemplate = () => {
7173
/>
7274
<Sidebar open={openSidebar} setOpen={setOpenSidebar} name={'Navigation Tools'}>
7375
<FormGroup>
74-
<FormControlLabel
75-
control={
76-
<Switch checked={alternativePosition} onChange={handleAlternativePositionChange} />
77-
}
78-
label="Alternative Position"
79-
/>
80-
<FormControlLabel
81-
control={<Switch checked={ThreeDButton} onChange={handleChange1} />}
82-
label="Show 2D/3D Button"
83-
/>
84-
<FormControlLabel
85-
control={<Switch checked={CenterLocationButton} onChange={handleChange2} />}
86-
label="Show CenterLocation Button"
87-
/>
88-
<FormControlLabel
89-
control={<Switch checked={ZoomButtons} onChange={handleChange3} />}
90-
label="Show Zoom Buttons"
91-
/>
92-
<FormControlLabel
93-
control={<Switch checked={FollowGpsButton} onChange={handleChange4} />}
94-
label="Show FollowGPS Button"
95-
/>
96-
<FormControlLabel
97-
control={<Switch checked={showCustomButton} onChange={handleCustomButtonChange} />}
98-
label="Add a custom Button"
99-
/>
76+
{tools.map((tool, text) => (
77+
<FormControlLabel
78+
key={text}
79+
control={
80+
<Switch checked={tool.const} onChange={() => tool.setter((current) => !current)} />
81+
}
82+
label={tool.text}
83+
/>
84+
))}
10085
</FormGroup>
10186
</Sidebar>
10287
<MlNavigationTools
10388
sx={alternativePosition ? { top: '80px' } : undefined}
104-
show3DButton={ThreeDButton}
105-
showCenterLocationButton={CenterLocationButton}
106-
showZoomButtons={ZoomButtons}
107-
showFollowGpsButton={FollowGpsButton}
89+
show3DButton={threeDButton}
90+
showGlobeButton={globeButton}
91+
showCenterLocationButton={centerLocationButton}
92+
showZoomButtons={zoomButtons}
93+
showFollowGpsButton={followGpsButton}
10894
>
10995
{showCustomButton ? (
11096
<Button
@@ -132,6 +118,12 @@ No3dButton.args = {
132118
show3DButton: false,
133119
};
134120

121+
export const ShowGlobeButton = Template.bind({});
122+
ShowGlobeButton.parameters = {};
123+
ShowGlobeButton.args = {
124+
showGlobeButton: true,
125+
};
126+
135127
export const ShowCenterLocationButton = Template.bind({});
136128
ShowCenterLocationButton.parameters = {};
137129
ShowCenterLocationButton.args = {

0 commit comments

Comments
 (0)