Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(netzkarte): add FloorSwitcher component #1212

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ REACT_APP_DEPARTURES_URL=//api.geops.io/sbb-departures/v1
REACT_APP_STATIC_FILES_URL=//maps.trafimage.ch
REACT_APP_REALTIME_URL=wss://api.geops.io/tracker-ws/v1/ws
REACT_APP_STOPS_URL=https://api.geops.io/stops/v1
REACT_APP_WALKING_URL=https://walking.geops.io/

# Consent management
REACT_APP_DOMAIN_CONSENT=trafimage.ch
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/permalink.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("permalink", () => {
),
);
cy.url().should("match", /lang=de/);
cy.url().should("match", /layers=&/);
cy.url().should("match", /layers=ch.sbb.geschosse2D&/);
// eslint-disable-next-line prefer-regex-literals
cy.url().should("match", new RegExp("x=928460&y=5908948&z=8.5"));
});
Expand Down
272 changes: 272 additions & 0 deletions src/components/FloorSwitcher/FloorSwitcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { getBottomLeft, getTopRight } from "ol/extent";
import { transform } from "ol/proj";
import { IconButton, List, ListItem } from "@mui/material";
import { Layer } from "mobility-toolbox-js/ol";
import { unByKey } from "ol/Observable";
import LayerService from "../../utils/LayerService";
import { FLOOR_LEVELS } from "../../utils/constants";

export const WALKING_BASE_URL = process.env.REACT_APP_WALKING_URL;

export const to4326 = (coord, decimal = 5) => {
return transform(coord, "EPSG:3857", "EPSG:4326").map((c) =>
parseFloat(c.toFixed(decimal)),
);
};

const propTypes = {
// mapStateToProps
center: PropTypes.arrayOf(PropTypes.number.isRequired),
zoom: PropTypes.number.isRequired,
map: PropTypes.object.isRequired,
layers: PropTypes.arrayOf(PropTypes.instanceOf(Layer)).isRequired,
activeTopic: PropTypes.string.isRequired,
};

class FloorSwitcher extends PureComponent {
constructor(props) {
super(props);

this.olListeners = [];
this.layerService = new LayerService();
this.state = {
floors: [],
activeFloor: "2D",
baseLayerHasLevelLayers: true,
};
this.onBaseLayerChange = this.onBaseLayerChange.bind(this);
this.abortController = new AbortController();
}

componentDidMount() {
this.initialize();
this.loadFloors();
}

componentDidUpdate(prevProps, prevState) {
const { center, zoom, layers, activeTopic } = this.props;
const { activeFloor, floors, baseLayerHasLevelLayers } = this.state;

if (prevProps.layers !== layers || prevProps.activeTopic !== activeTopic) {
this.initialize();
}

if (
prevProps.center !== center ||
prevState.baseLayerHasLevelLayers !== baseLayerHasLevelLayers
) {
this.loadFloors();
}

if (prevState.floors !== floors && !floors.includes(activeFloor)) {
// Reset to 2D when the active floor is no longer in the extent floors and on topic change
this.selectFloor("2D");
}

if (prevProps.zoom !== zoom) {
// Apply 2D floor when zoom is less than 16, use state floor otherwise
this.selectFloor(zoom <= 16 ? "2D" : activeFloor, false);
}
}

componentWillUnmount() {
// Reset when a topic without floors is loaded
this.selectFloor("2D");
this.removeMapListeners();
}

onBaseLayerChange() {
const baseLayers = this.layerService.getBaseLayers();
const visibleBaselayer = baseLayers.find((l) => l.visible);
const levelLayerBaselayers = this.layerService
.getLayer("ch.sbb.geschosse")
?.children[0]?.get("baselayers");

this.setState({
baseLayerHasLevelLayers: levelLayerBaselayers?.includes(
visibleBaselayer || baseLayers[0],
),
});
}

getVisibleLevelLayer() {
return this.layerService
.getLayer("ch.sbb.geschosse")
?.children.find((l) => l.visible);
}

addMapListeners() {
this.removeMapListeners();
const baselayers = this.layerService.getBaseLayers();
baselayers.forEach((layer) => {
this.olListeners.push(layer.on("change:visible", this.onBaseLayerChange));
});
}

initialize() {
const { layers, zoom } = this.props;
this.layerService.setLayers(layers);
this.onBaseLayerChange();
this.addMapListeners();
const visibleLevelLayer = this.getVisibleLevelLayer();

if (!visibleLevelLayer || zoom < 16) {
this.selectFloor("2D");
return;
}
this.selectFloor(visibleLevelLayer.level);
}

removeMapListeners() {
unByKey(this.olListeners);
}

loadFloors() {
const { baseLayerHasLevelLayers } = this.state;
const { map, zoom } = this.props;
this.abortController.abort();
this.abortController = new AbortController();
const { signal } = this.abortController;

if (!baseLayerHasLevelLayers || zoom <= 16 || !WALKING_BASE_URL) {
this.setState({
floors: [],
});
return;
}

const extent = map.getView().calculateExtent();
const reqUrl = `${WALKING_BASE_URL}availableLevels?bbox=${to4326(
getBottomLeft(extent),
)
.reverse()
.join(",")}|${to4326(getTopRight(extent)).reverse().join(",")}`;
fetch(reqUrl, { signal })
.then((response) => response.json())
.then((response) => {
const floors = response.properties.availableLevels.filter((level) =>
FLOOR_LEVELS.includes(level),
);
if (!floors.includes("2D")) {
floors.splice(floors.indexOf(0) + 1, 0, "2D");
}
this.setState({
floors: floors.reverse(),
});
})
.catch((err) => {
if (err.name === "AbortError") {
// eslint-disable-next-line no-console
console.warn(`Abort ${reqUrl}`);
return;
}
// It's important to rethrow all other errors so you don't silence them!
// For example, any error thrown by setState(), will pass through here.
throw err;
});
}

selectFloor(floor, shouldSetState = true) {
this.layerService
.getLayer(`ch.sbb.geschosse`)
?.children.forEach((layer) => {
// eslint-disable-next-line no-param-reassign
layer.visible = false;
});

const layer = this.layerService.getLayer(`ch.sbb.geschosse${floor}`);
if (layer) {
layer.visible = true;
if (shouldSetState) {
this.setState({ activeFloor: floor });
}
}
}

render() {
const { zoom } = this.props;
const { floors, activeFloor, baseLayerHasLevelLayers } = this.state;

if (
!zoom || // When app is loaded without z param
zoom <= 16 ||
floors?.length <= 2 ||
!baseLayerHasLevelLayers
) {
return null;
}

return (
<List
className="wkp-floor-switcher"
sx={{
boxShadow: "0 0 7px rgba(0, 0, 0, 0.9)",
borderRadius: "20px",
overflow: "hidden",
backgroundColor: "white",
gap: "2px",
display: "flex",
flexDirection: "column",
transition: "box-shadow 0.5s ease",
"&:hover": {
boxShadow: "0 0 12px 2px rgba(0, 0, 0, 0.9)",
},
}}
>
{floors.map((floor) => {
const backgroundColor = floor === "2D" ? "#e8e7e7" : "white";
return (
<ListItem
key={floor}
disablePadding
sx={{
width: 40,
height: 40,
padding: 0.5,
backgroundColor,
}}
>
<IconButton
onClick={() => this.selectFloor(floor)}
sx={{
typography: "body1",
borderRadius: "50%",
backgroundColor:
activeFloor === floor ? "#444" : backgroundColor,
border: 0,
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
color: floor === activeFloor ? "white" : "#444",
"&:hover": {
color: floor === activeFloor ? "white" : "secondary.dark",
backgroundColor:
activeFloor === floor ? "#444" : backgroundColor,
},
}}
>
{floor}
</IconButton>
</ListItem>
);
})}
</List>
);
}
}

const mapStateToProps = (state) => ({
center: state.map.center,
zoom: state.map.zoom,
map: state.app.map,
layers: state.map.layers,
activeTopic: state.app.activeTopic,
});

FloorSwitcher.propTypes = propTypes;

export default connect(mapStateToProps)(FloorSwitcher);
1 change: 1 addition & 0 deletions src/components/FloorSwitcher/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./FloorSwitcher";
4 changes: 4 additions & 0 deletions src/components/MapControls/MapControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import { ReactComponent as ZoomOut } from "../../img/minus.svg";
import { ReactComponent as ZoomIn } from "../../img/plus.svg";
import useHasScreenSize from "../../utils/useHasScreenSize";
import { setZoomType } from "../../model/map/actions";
import FloorSwitcher from "../FloorSwitcher";

const propTypes = {
geolocation: PropTypes.bool,
zoomSlider: PropTypes.bool,
fitExtent: PropTypes.bool,
menuToggler: PropTypes.bool,
floorSwitcher: PropTypes.bool,
children: PropTypes.node,
};

Expand Down Expand Up @@ -85,6 +87,7 @@ function MapControls({
geolocation = true,
zoomSlider = true,
fitExtent = true,
floorSwitcher = false,
children,
}) {
const dispatch = useDispatch();
Expand Down Expand Up @@ -172,6 +175,7 @@ function MapControls({
/>
{geolocation && <Geolocation />}
{fitExtent && <FitExtent />}
{floorSwitcher && <FloorSwitcher />}
{children}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/TopicElements/TopicElements.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ function TopicElements({ history = null }) {
geolocation={elements.geolocationButton}
fitExtent={elements.fitExtent}
zoomSlider={elements.zoomSlider}
floorSwitcher={elements.floorSwitcher}
>
{activeTopic.mapControls}
</MapControls>
Expand Down
13 changes: 11 additions & 2 deletions src/components/TopicMenu/TopicMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ class TopicMenu extends PureComponent {
layerTree = (
<div className="wkp-layer-tree">
<LayerTree
isItemHidden={(l) => l.get("isBaseLayer") || l.get("hideInLegend")}
isItemHidden={(l) =>
l.get("isBaseLayer") ||
l.get("hideInLegend") ||
l.get("hideInLayerTree")
}
layers={layers}
t={t}
titles={this.titles}
Expand Down Expand Up @@ -184,8 +188,13 @@ class TopicMenu extends PureComponent {
const collapsed = isCollapsed || activeTopic.key !== topic.key;
const isActiveTopic = topic.key === activeTopic.key;
const isMenuVisibleLayers = (topic.layers || []).find((l) => {
return !l.get("hideInLegend");
return (
!l.get("hideInLegend") &&
!l.get("isBaseLayer") &&
danji90 marked this conversation as resolved.
Show resolved Hide resolved
!l.get("hideInLayerTree")
);
});

const baseLayers = new LayerService(layers).getBaseLayers();
const currentBaseLayer = baseLayers.find((l) => l.visible);

Expand Down
1 change: 1 addition & 0 deletions src/components/TopicsMenuHeader/TopicsMenuHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function TopicsMenuHeader({ isOpen = false, onToggle }) {
const topicLayers = flatLayers.reverse().filter((l) => {
return (
!l.get("isBaseLayer") &&
!l.get("hideInLayerTree") &&
!l.get("hideInLegend") &&
!flatLayers.includes(l.parent) // only root layers
);
Expand Down
Loading