Skip to content

Commit 8dbedc8

Browse files
authored
add map layer failure fallback (#2708)
1 parent 9105676 commit 8dbedc8

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

src/components/EnhancedMap/SourcedTileLayer/SourcedTileLayer.jsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
normalizeLayer,
1212
} from "../../../services/VisibleLayer/LayerSources";
1313
import WithErrors from "../../HOCs/WithErrors/WithErrors";
14+
import TileLayerErrorBoundary from "../../TaskClusterMap/TileLayerErrorBoundary";
1415

1516
/**
1617
* SourcedTileLayer renders a react-leaflet TileLayer from the current
@@ -21,7 +22,7 @@ import WithErrors from "../../HOCs/WithErrors/WithErrors";
2122
*
2223
* @author [Neil Rotstan](https://github.com/nrotstan)
2324
*/
24-
const SourcedTileLayer = (props) => {
25+
const SourcedTileLayerInternal = (props) => {
2526
const [layerRenderFailed, setLayerRenderFailed] = useState(false);
2627

2728
const attribution = (layer) => {
@@ -35,7 +36,7 @@ const SourcedTileLayer = (props) => {
3536
};
3637

3738
useEffect(() => {
38-
if (layerRenderFailed && currentLayer) {
39+
if (layerRenderFailed) {
3940
setLayerRenderFailed(false);
4041
}
4142
}, [props.source.id]);
@@ -45,7 +46,7 @@ const SourcedTileLayer = (props) => {
4546
}
4647

4748
if (layerRenderFailed) {
48-
if (fallbackLayer) {
49+
if (props.fallbackLayer) {
4950
return (
5051
<FormattedMessage
5152
{...AppErrors.map.renderFailure}
@@ -59,13 +60,20 @@ const SourcedTileLayer = (props) => {
5960

6061
const normalizedLayer = normalizeLayer(props.source);
6162

63+
const handleTileError = () => {
64+
setLayerRenderFailed(true);
65+
};
66+
6267
if (normalizedLayer.type === "bing") {
6368
return (
6469
<BingLayer
6570
key={normalizedLayer.id}
6671
{...normalizedLayer}
6772
type="Aerial"
6873
attribution={attribution(normalizedLayer)}
74+
eventHandlers={{
75+
tileerror: handleTileError,
76+
}}
6977
/>
7078
);
7179
}
@@ -75,20 +83,34 @@ const SourcedTileLayer = (props) => {
7583
key={normalizedLayer.id}
7684
{...normalizedLayer}
7785
attribution={attribution(normalizedLayer)}
86+
eventHandlers={{
87+
tileerror: handleTileError,
88+
}}
7889
{...props}
7990
/>
8091
);
8192
};
8293

94+
const SourcedTileLayer = (props) => {
95+
return (
96+
<TileLayerErrorBoundary sourceId={props.source?.id}>
97+
<SourcedTileLayerInternal {...props} />
98+
</TileLayerErrorBoundary>
99+
);
100+
};
101+
83102
SourcedTileLayer.propTypes = {
84103
/** LayerSource to use */
85104
source: layerSourceShape,
86105
/** Set to true to suppress display of source attribution */
87106
skipAttribution: PropTypes.bool,
107+
/** Set to true if this is a fallback layer */
108+
fallbackLayer: PropTypes.bool,
88109
};
89110

90111
SourcedTileLayer.defaultProps = {
91112
skipAttribution: false,
113+
fallbackLayer: false,
92114
};
93115

94116
export default WithErrors(injectIntl(SourcedTileLayer));
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import PropTypes from "prop-types";
2+
import React from "react";
3+
import { FormattedMessage } from "react-intl";
4+
import AppErrors from "../../services/Error/AppErrors";
5+
6+
/**
7+
* Error boundary component that provides a black screen fallback
8+
* when tile layer rendering fails
9+
*/
10+
class TileLayerErrorBoundary extends React.Component {
11+
constructor(props) {
12+
super(props);
13+
this.state = { hasError: false };
14+
}
15+
16+
componentDidUpdate(prevProps) {
17+
if (this.state.hasError && prevProps.sourceId !== this.props.sourceId) {
18+
this.setState({ hasError: false });
19+
}
20+
}
21+
22+
static getDerivedStateFromError(error) {
23+
return { hasError: true };
24+
}
25+
26+
componentDidCatch(error, errorInfo) {
27+
console.error("Tile layer error:", error, errorInfo);
28+
}
29+
30+
render() {
31+
if (this.state.hasError) {
32+
return (
33+
<div
34+
style={{
35+
position: "absolute",
36+
top: 0,
37+
left: 0,
38+
right: 0,
39+
bottom: 0,
40+
backgroundColor: "#000000",
41+
zIndex: 1,
42+
display: "flex",
43+
alignItems: "center",
44+
justifyContent: "center",
45+
color: "#ffffff",
46+
fontSize: "14px",
47+
padding: "60px",
48+
fontFamily: "Arial, sans-serif",
49+
}}
50+
>
51+
<div style={{ textAlign: "center" }}>
52+
<FormattedMessage
53+
{...AppErrors.map.renderFailure}
54+
values={{ details: "map layer unavailable" }}
55+
/>
56+
<div style={{ marginTop: "10px", fontSize: "12px", opacity: 0.8 }}>
57+
<FormattedMessage
58+
id="TileLayerErrorBoundary.retryMessage"
59+
defaultMessage="Please refresh the page to retry"
60+
/>
61+
</div>
62+
</div>
63+
</div>
64+
);
65+
}
66+
67+
return this.props.children;
68+
}
69+
}
70+
71+
TileLayerErrorBoundary.propTypes = {
72+
children: PropTypes.node.isRequired,
73+
sourceId: PropTypes.string,
74+
};
75+
76+
export default TileLayerErrorBoundary;

0 commit comments

Comments
 (0)