• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

terrestris / react-geo / 18274503963

06 Oct 2025 08:18AM UTC coverage: 63.354%. Remained the same
18274503963

push

github

web-flow
Merge pull request #4396 from terrestris/dependabot/npm_and_yarn/commitlint/cli-20.1.0

build(deps-dev): bump @commitlint/cli from 19.8.1 to 20.1.0

597 of 1040 branches covered (57.4%)

Branch coverage included in aggregate %.

1137 of 1697 relevant lines covered (67.0%)

11.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

38.64
/src/BackgroundLayerChooser/BackgroundLayerChooser.tsx
1
import React, {
2
  useEffect,
3
  useRef,
4
  useState
5
} from 'react';
6

7
import {
8
  faBan,
9
  faChevronLeft,
10
  faChevronRight
11
} from '@fortawesome/free-solid-svg-icons';
12
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
13

14
import OlOverviewMap from 'ol/control/OverviewMap';
15
import OlLayerBase from 'ol/layer/Base';
16
import OlLayerGroup from 'ol/layer/Group';
17
import OlLayerImage from 'ol/layer/Image';
18
import OlLayer from 'ol/layer/Layer';
19
import OlLayerTile from 'ol/layer/Tile';
20
import { ObjectEvent } from 'ol/Object';
21
import OlSourceImageWMS from 'ol/source/ImageWMS';
22
import OlSourceOSM from 'ol/source/OSM';
23
import OlSourceTileWMS from 'ol/source/TileWMS';
24
import OlSourceWMTS from 'ol/source/WMTS';
25
import OlTilegridWMTS from 'ol/tilegrid/WMTS';
26
import { getUid } from 'ol/util';
27
import OlView from 'ol/View';
28

29
import { apply as applyMapboxStyle } from 'ol-mapbox-style';
30

31
import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap';
32

33
import BackgroundLayerPreview from '../BackgroundLayerPreview/BackgroundLayerPreview';
34
import SimpleButton from '../Button/SimpleButton/SimpleButton';
35

36
import './BackgroundLayerChooser.less';
37

38
export interface BackgroundLayerChooserProps {
39
  /**
40
   * Array of layers to be displayed in the BackgroundLayerChooser.
41
   */
42
  layers: OlLayer[];
43
  /**
44
   * Adds a button that clears the backgroundlayer.
45
   */
46
  allowEmptyBackground?: boolean;
47
  /**
48
   * Filters the backgroundlayers by a function.
49
   */
50
  backgroundLayerFilter?: (layer: OlLayerBase) => boolean;
51
  /**
52
   * Select a Layer that should be active initially.
53
   */
54
  initiallySelectedLayer?: OlLayer;
55
  /**
56
   * Customize the tooltip.
57
   */
58
  buttonTooltip?: string;
59
  /**
60
   * Sets the title of the No-Background Button
61
   */
62
  noBackgroundTitle?: string;
63
  /**
64
   * A function that renders the title of the layer.
65
   * If not provided, the layer's name will be used.
66
   */
67
  titleRenderer?: (layer: OlLayer) => React.ReactNode;
68
}
69

70
/**
71
 * This component supports TileWMS and ImageWMS layers. Besides that, mapbox vector tile layers are
72
 * also supported in a limited way:
73
 *
74
 * * you'll need to render the vector tile layer inside of a group layer
75
 * * the group layer needs to have a property isVectorTile set to true
76
 * * the group layer needs to have a property url pointing to the json description
77
 */
78
export const BackgroundLayerChooser: React.FC<BackgroundLayerChooserProps> = ({
1✔
79
  layers,
80
  allowEmptyBackground = false,
16✔
81
  buttonTooltip = 'Change background layer',
27✔
82
  initiallySelectedLayer,
83
  noBackgroundTitle = 'No Background',
24✔
84
  backgroundLayerFilter = (l: OlLayerBase) => !!l.get('isBackgroundLayer'),
✔
85
  titleRenderer
86
}) => {
87
  const map = useMap();
27✔
88

89
  const [zoom, setZoom] = useState(map?.getView()?.getZoom());
27✔
90
  const [center, setCenter] = useState(map?.getView()?.getCenter());
27✔
91
  const [layerOptionsVisible, setLayerOptionsVisible] = useState<boolean>(false);
27✔
92
  const [selectedLayer, setSelectedLayer] = useState<OlLayer>();
27✔
93
  const [isBackgroundImage, setIsBackgroundImage] = useState<boolean>(false);
27✔
94

95
  const mapTarget = useRef(null);
27✔
96

97
  useEffect(() => {
27✔
98
    if (map && layerOptionsVisible) {
16!
99
      setCenter(map.getView().getCenter());
×
100
      setZoom(map.getView().getZoom());
×
101
      const centerListener = (evt: ObjectEvent) => {
×
102
        setCenter(evt.target.getCenter());
×
103
      };
104
      const resolutionListener = (evt: ObjectEvent) => {
×
105
        setZoom(evt.target.getZoom());
×
106
      };
107
      map.getView().on('change:center', centerListener);
×
108
      map.getView().on('change:resolution', resolutionListener);
×
109
      return () => {
×
110
        map.getView().un('change:center', centerListener);
×
111
        map.getView().un('change:resolution', resolutionListener);
×
112
      };
113
    }
114
    return undefined;
16✔
115
  }, [map, layerOptionsVisible]);
116

117
  useEffect(() => {
27✔
118
    const activeLayerCand = layers.find(l => l.getVisible());
11✔
119

120
    if (!initiallySelectedLayer) {
11!
121
      setSelectedLayer(activeLayerCand as OlLayer);
11✔
122
    }
123
  }, [initiallySelectedLayer, layers]);
124

125
  useEffect(() => {
27✔
126
    if (!selectedLayer || !map) {
23!
127
      return undefined;
23✔
128
    }
129
    const selectedLayerSource = selectedLayer.getSource();
×
130

131
    let ovLayer: OlLayer | OlLayerGroup | null = null;
×
132

133
    if (selectedLayer instanceof OlLayerTile) {
×
134
      let newSource: OlSourceOSM | OlSourceTileWMS | OlSourceWMTS | null = null;
×
135

136
      if (selectedLayerSource instanceof OlSourceTileWMS) {
×
137
        newSource = new OlSourceTileWMS({
×
138
          url: selectedLayerSource.getUrls()?.[0],
139
          params: selectedLayerSource.getParams(),
140
          tileLoadFunction: selectedLayerSource.getTileLoadFunction()
141
        });
142
      } else if (selectedLayerSource instanceof OlSourceOSM) {
×
143
        newSource = new OlSourceOSM();
×
144
      } else if (selectedLayerSource instanceof OlSourceWMTS) {
×
145
        const newTileGrid = selectedLayerSource.getTileGrid() as OlTilegridWMTS | null;
×
146

147
        if (!newTileGrid) {
×
148
          return;
×
149
        }
150

151
        newSource = new OlSourceWMTS({
×
152
          url: selectedLayerSource.getUrls()?.[0],
153
          layer: selectedLayerSource.getLayer(),
154
          matrixSet: selectedLayerSource.getMatrixSet(),
155
          format: selectedLayerSource.getFormat(),
156
          tileGrid: newTileGrid,
157
          style: selectedLayerSource.getStyle(),
158
          requestEncoding: selectedLayerSource.getRequestEncoding(),
159
          version: selectedLayerSource.getVersion(),
160
          dimensions: selectedLayerSource.getDimensions(),
161
          wrapX: selectedLayerSource.getWrapX()
162
        });
163
      }
164

165
      if (newSource) {
×
166
        ovLayer = new OlLayerTile({
×
167
          source: newSource
168
        });
169
      }
170
    } else if (selectedLayer instanceof OlLayerImage) {
×
171
      let newSource: OlSourceImageWMS | null = null;
×
172

173
      if (selectedLayerSource instanceof OlSourceImageWMS) {
×
174
        newSource = new OlSourceImageWMS({
×
175
          url: selectedLayerSource.getUrl(),
176
          params: selectedLayerSource.getParams(),
177
          imageLoadFunction: selectedLayerSource.getImageLoadFunction()
178
        });
179
      }
180

181
      if (newSource) {
×
182
        ovLayer = new OlLayerImage({
×
183
          source: selectedLayer.getSource()
184
        });
185
      }
186
    } else if (selectedLayer instanceof OlLayerGroup) {
×
187
      if (selectedLayer.get('isVectorTile')) {
×
188
        ovLayer = new OlLayerGroup();
×
189
        applyMapboxStyle(ovLayer, selectedLayer.get('url'));
×
190
      } else {
191
        ovLayer = new OlLayerGroup({
×
192
          layers: selectedLayer.getLayers()
193
        });
194
      }
195
    }
196

197
    if (ovLayer && mapTarget.current) {
×
198
      const overViewControl = new OlOverviewMap({
×
199
        collapsible: false,
200
        target: mapTarget.current,
201
        className: 'ol-overviewmap react-geo-bg-layer-chooser-overviewmap',
202
        layers: [ovLayer],
203
        view: new OlView({
204
          projection: map.getView().getProjection()
205
        })
206
      });
207

208
      map.addControl(overViewControl);
×
209

210
      return () => {
×
211
        map.removeControl(overViewControl);
×
212
      };
213
    }
214

215
    return undefined;
×
216
  }, [selectedLayer, map]);
217

218
  const onLayerSelect = (layer: OlLayer) => {
27✔
219
    setLayerOptionsVisible(false);
×
220
    setSelectedLayer(layer);
×
221
    setIsBackgroundImage(false);
×
222
  };
223

224
  return (
27✔
225
    <div className="bg-layer-chooser">
226
      {
227
        layerOptionsVisible && (
30✔
228
          <div className="layer-cards">
229
            {
230
              layers.map(layer => (
231
                <BackgroundLayerPreview
6✔
232
                  key={getUid(layer)}
233
                  activeLayer={selectedLayer}
234
                  onClick={l => onLayerSelect(l)}
×
235
                  layer={layer}
236
                  backgroundLayerFilter={backgroundLayerFilter}
237
                  zoom={zoom}
238
                  center={center}
239
                  titleRenderer={titleRenderer}
240
                />
241
              ))
242
            }
243
            {
244
              allowEmptyBackground &&
5✔
245
                <div
246
                  className={`no-background${selectedLayer ? '' : ' selected'}`}
2!
247
                  onMouseOver={() => {
248
                    selectedLayer?.setVisible(false);
×
249
                  }}
250
                  onMouseLeave={() => {
251
                    selectedLayer?.setVisible(true);
×
252
                  }}
253
                  onClick={() => {
254
                    selectedLayer?.setVisible(false);
1✔
255
                    setSelectedLayer(undefined);
1✔
256
                    setLayerOptionsVisible(false);
1✔
257
                    setIsBackgroundImage(true);
1✔
258
                  }}
259
                >
260
                  <div
261
                    className="no-background-preview"
262
                  >
263
                    <FontAwesomeIcon
264
                      icon={faBan}
265
                    />
266
                  </div>
267
                  <span
268
                    className="layer-title"
269
                  >
270
                    {noBackgroundTitle}
271
                  </span>
272
                </div>
273
            }
274
          </div>
275
        )
276
      }
277
      <SimpleButton
278
        className={`change-bg-btn${layerOptionsVisible ? ' toggled' : ''}`}
27✔
279
        size="small"
280
        tooltip={buttonTooltip}
281
        icon={layerOptionsVisible ?
27✔
282
          <FontAwesomeIcon
283
            icon={faChevronRight}
284
          /> :
285
          <FontAwesomeIcon
286
            icon={faChevronLeft}
287
          />
288
        }
289
        onClick={() => setLayerOptionsVisible(!layerOptionsVisible)}
4✔
290
      />
291
      <div
292
        className="bg-preview"
293
      >
294
        {
295
          !isBackgroundImage ?
27✔
296
            <div
297
              id="overview-map"
298
              ref={mapTarget}
299
            /> :
300
            <div
301
              className="no-background-preview"
302
            >
303
              <FontAwesomeIcon
304
                icon={faBan}
305
              />
306
            </div>
307
        }
308
        {
309
          selectedLayer ?
27✔
310
            <span
311
              className="layer-title"
312
            >
313
              {titleRenderer ? titleRenderer(selectedLayer) : selectedLayer.get('name')}
15✔
314
            </span> :
315
            <span
316
              className="layer-title"
317
            >
318
              {noBackgroundTitle}
319
            </span>
320
        }
321
      </div>
322
    </div>
323
  );
324
};
325

326
export default BackgroundLayerChooser;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc