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

terrestris / react-geo / 12949049361

24 Jan 2025 11:49AM UTC coverage: 57.421% (+0.2%) from 57.197%
12949049361

Pull #4070

github

web-flow
Merge 0cd21609d into 91a1ab539
Pull Request #4070: chore(eslint): upgrade to v9 and fix resulting lint errors / issues

484 of 969 branches covered (49.95%)

Branch coverage included in aggregate %.

16 of 21 new or added lines in 14 files covered. (76.19%)

1 existing line in 1 file now uncovered.

1021 of 1652 relevant lines covered (61.8%)

11.24 hits per line

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

44.01
/src/Panel/TimeLayerSliderPanel/TimeLayerSliderPanel.tsx
1
import './TimeLayerSliderPanel.less';
2

3
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
4

5
import {
6
  faCalendar,
7
  faPauseCircle,
8
  faPlayCircle,
9
  faSync
10
} from '@fortawesome/free-solid-svg-icons';
11
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12

13
import { DatePicker, Popover, Select, Spin } from 'antd';
14
import dayjs from 'dayjs';
15
import { debounce } from 'lodash';
16
import _isEqual from 'lodash/isEqual';
17
import _isFinite from 'lodash/isFinite';
18
import moment, { Moment } from 'moment';
19
import { getUid } from 'ol';
20
import { TileWMS } from 'ol/source';
21
import ImageWMS from 'ol/source/ImageWMS';
22

23
import { WmsLayer } from '@terrestris/ol-util/dist/typeUtils/typeUtils';
24
import { TimeLayerAwareConfig } from '@terrestris/react-util/dist/Hooks/useTimeLayerAware/useTimeLayerAware';
25

26
import SimpleButton from '../../Button/SimpleButton/SimpleButton';
27
import ToggleButton from '../../Button/ToggleButton/ToggleButton';
28
import TimeSlider from '../../Slider/TimeSlider/TimeSlider';
29

30
const RangePicker = DatePicker.RangePicker;
1✔
31
const Option = Select.Option;
1✔
32

33
export interface Tooltips {
34
  hours: string;
35
  days: string;
36
  weeks: string;
37
  months: string;
38
  years: string;
39
  setToMostRecent: string;
40
  dataRange: string;
41
}
42

43
export type PlaybackSpeedType = 'hours' | 'days' | 'weeks' | 'months' | 'years';
44

45
export interface TimeLayerSliderPanelProps {
46
  className?: string;
47
  onChange?: (arg: moment.Moment) => void;
48
  timeAwareLayers: WmsLayer[];
49
  value?: moment.Moment;
50
  dateFormat?: string;
51
  tooltips?: Tooltips;
52
  autoPlaySpeedOptions?: number[];
53
  initStartDate?: moment.Moment;
54
  initEndDate?: moment.Moment;
55
}
56

57
/**
58
 * The panel combining all time slider related parts.
59
 */
60
export const TimeLayerSliderPanel: React.FC<TimeLayerSliderPanelProps> = memo(
1✔
61
  ({
62
    className = '',
8✔
63
    onChange = () => undefined,
7✔
64
    timeAwareLayers = [],
×
65
    value = moment(moment.now()),
8✔
66
    dateFormat = 'YYYY-MM-DD HH:mm',
8✔
67
    tooltips = {
8✔
68
      setToMostRecent: 'Set to most recent date',
69
      hours: 'Hours',
70
      days: 'Days',
71
      weeks: 'Weeks',
72
      months: 'Months',
73
      years: 'Years',
74
      dataRange: 'Set data range'
75
    },
76
    autoPlaySpeedOptions = [0.5, 1, 2, 5, 10, 100, 300],
8✔
77
    initStartDate = moment(moment.now()),
×
78
    initEndDate = moment(moment.now()).add(1, 'days')
×
79
  }) => {
80
    const [currentValue, setCurrentValue] = useState<Moment>(value);
8✔
81
    const [playbackSpeed, setPlaybackSpeed] = useState<
8✔
82
      number | PlaybackSpeedType
83
    >(1);
84
    const [autoPlayActive, setAutoPlayActive] = useState(false);
8✔
85
    const [startDate, setStartDate] = useState<Moment>(initStartDate);
8✔
86
    const [endDate, setEndDate] = useState<Moment>(initEndDate);
8✔
87
    const [loadingCount, setLoadingCount] = useState(0);
8✔
88

89
    const wmsTimeLayersRef = useRef<TimeLayerAwareConfig[]>([]);
8✔
90
    const intervalRef = useRef<number | undefined>(1000);
8✔
91
    const prevPropsRef = useRef<TimeLayerSliderPanelProps>();
8✔
92

93
    const isLoading = loadingCount > 0;
8✔
94

95
    const wrapTimeSlider = useCallback(() => {
8✔
96
      const wmsTimeLayers: TimeLayerAwareConfig[] = [];
3✔
97
      timeAwareLayers.forEach(l => {
3✔
98
        if (l.get('type') === 'WMSTime') {
1!
99
          wmsTimeLayers.push({ layer: l });
×
100
        }
101
      });
102
      wmsTimeLayersRef.current = wmsTimeLayers;
3✔
103
    }, [timeAwareLayers]);
104

105
    const autoPlay = useCallback(() => {
8✔
106
      setAutoPlayActive(prevState => !prevState);
3✔
107
    }, []);
108

109
    const onTimeChanged = useCallback(
8✔
110
      (val: string | [string, string]) => {
111
        const newTime = moment(val);
×
112
        setCurrentValue(newTime);
×
113
        if (onChange) {
×
114
          onChange(newTime);
×
115
        }
116
        debouncedWmsTimeHandlerRef.current(val);
×
117
      },
118
      [onChange]
119
    );
120

121
    const updateDataRange = useCallback(([start, end]: [Moment, Moment]) => {
8✔
122
      setStartDate(start);
×
123
      setEndDate(end);
×
124
    }, []);
125

126
    const setSliderToMostRecent = useCallback(() => {
8✔
127
      setCurrentValue(initEndDate);
3✔
128
      setEndDate(initEndDate);
3✔
129
      wmsTimeHandler(initEndDate);
3✔
130
    }, [initEndDate]);
131

132
    const onPlaybackSpeedChange = useCallback(
8✔
133
      (val: number | PlaybackSpeedType) => {
134
        setPlaybackSpeed(val);
×
135
      },
136
      []
137
    );
138

139
    const wmsTimeHandler = (val: moment.Moment | string | [string, string]) => {
8✔
140
      wmsTimeLayersRef.current.forEach(config => {
3✔
141
        if (config.layer && config.layer.get('type') === 'WMSTime') {
×
142
          const source = config.layer.getSource();
×
143
          const params = source?.getParams();
×
144
          let time;
145
          if (Array.isArray(val)) {
×
146
            time = val[0];
×
147
          } else {
148
            time = val;
×
149
          }
150
          if (!moment.isMoment(time)) {
×
151
            time = moment(time);
×
152
          }
153
          const timeFormat = config.layer.get('timeFormat');
×
154
          let newTimeParam: string;
155
          if (
×
156
            timeFormat.toLowerCase().includes('hh') &&
×
157
            config.layer.get('roundToFullHours')
158
          ) {
159
            time.set('minute', 0);
×
160
            time.set('second', 0);
×
161
            newTimeParam = time.toISOString();
×
162
          } else {
163
            newTimeParam = time.format(timeFormat);
×
164
          }
165

166
          if (params.TIME !== newTimeParam) {
×
167
            params.TIME = newTimeParam;
×
168
            source?.updateParams(params);
×
169
            source?.refresh();
×
170
          }
171
        }
172
      });
173
    };
174

175
    const debouncedWmsTimeHandlerRef = useRef(
8✔
176
      debounce(val => wmsTimeHandler(val), 300)
×
177
    );
178

179
    useEffect(() => {
8✔
180
      const handler = debouncedWmsTimeHandlerRef.current;
3✔
181

182
      return () => {
3✔
183
        handler.cancel();
3✔
184
      };
185
    }, []);
186

187
    const timeSliderCustomHandler = useCallback(
8✔
188
      (val: any) => {
189
        const currentMoment = moment(val).milliseconds(0);
7✔
190
        if (!currentMoment.isSame(currentValue)) {
7!
191
          const newValue = currentMoment.clone();
7✔
192
          if (onChange) {
7!
193
            onChange(newValue);
7✔
194
          }
195
        }
196
      },
197
      [currentValue, onChange]
198
    );
199

200
    const findRangeForLayers = useCallback(() => {
8✔
201
      if (timeAwareLayers.length === 0) {
×
202
        return;
×
203
      }
204

205
      const startDatesFromLayers: moment.Moment[] = [];
×
206
      const endDatesFromLayers: moment.Moment[] = [];
×
207

208
      timeAwareLayers.forEach(l => {
×
209
        const layerType = l.get('type');
×
210
        if (layerType === 'WMSTime') {
×
211
          const layerStartDate = l.get('startDate');
×
212
          const layerEndDate = l.get('endDate');
×
213
          let sdm;
214
          let edm;
215
          if (layerStartDate) {
×
216
            sdm = moment(layerStartDate);
×
217
          }
218
          if (layerEndDate) {
×
219
            edm = moment(layerEndDate);
×
220
          }
221
          if (sdm) {
×
222
            startDatesFromLayers.push(sdm);
×
223
          }
224
          if (edm) {
×
225
            endDatesFromLayers.push(edm);
×
226
          }
227
        }
228
      });
229

230
      const newStartDate =
231
        startDatesFromLayers.length > 0
×
232
          ? moment.min(startDatesFromLayers)
233
          : startDate;
234
      const newEndDate =
235
        endDatesFromLayers.length > 0
×
236
          ? moment.max(endDatesFromLayers)
237
          : endDate;
238

239
      updateDataRange([newStartDate, newEndDate]);
×
240
    }, [timeAwareLayers, startDate, endDate, updateDataRange]);
241

242
    useEffect(() => {
8✔
243
      if (timeAwareLayers.length === 0) {
3✔
244
        return;
2✔
245
      }
246

247
      const handleTileLoadStart = () => {
1✔
248
        setLoadingCount(prevCount => prevCount + 1);
×
249
      };
250
      const handleTileLoadEnd = () => {
1✔
251
        setLoadingCount(prevCount => Math.max(prevCount - 1, 0));
×
252
      };
253
      const handleImageLoadStart = () => {
1✔
254
        setLoadingCount(prevCount => prevCount + 1);
×
255
      };
256
      const handleImageLoadEnd = () => {
1✔
257
        setLoadingCount(prevCount => Math.max(prevCount - 1, 0));
×
258
      };
259

260
      timeAwareLayers.forEach(layer => {
1✔
261
        if (layer.get('type') === 'WMSTime') {
1!
262
          const source = layer.getSource();
×
263

264
          if (source instanceof TileWMS) {
×
265
            source.on('tileloadstart', handleTileLoadStart);
×
266
            source.on('tileloadend', handleTileLoadEnd);
×
267
            source.on('tileloaderror', handleTileLoadEnd);
×
268
          } else if (source instanceof ImageWMS) {
×
269
            source.on('imageloadstart', handleImageLoadStart);
×
270
            source.on('imageloadend', handleImageLoadEnd);
×
271
            source.on('imageloaderror', handleImageLoadEnd);
×
272
          }
273
        }
274
      });
275

276
      return () => {
1✔
277
        timeAwareLayers.forEach(layer => {
1✔
278
          if (layer.get('type') === 'WMSTime') {
1!
279
            const source = layer.getSource();
×
280

281
            if (source instanceof TileWMS) {
×
282
              source.un('tileloadstart', handleTileLoadStart);
×
283
              source.un('tileloadend', handleTileLoadEnd);
×
284
              source.un('tileloaderror', handleTileLoadEnd);
×
285
            } else if (source instanceof ImageWMS) {
×
286
              source.un('imageloadstart', handleImageLoadStart);
×
287
              source.un('imageloadend', handleImageLoadEnd);
×
288
              source.un('imageloaderror', handleImageLoadEnd);
×
289
            }
290
          }
291
        });
292
      };
293
    }, [timeAwareLayers]);
294

295
    useEffect(() => {
8✔
296
      window.clearInterval(intervalRef.current);
7✔
297
      if (!autoPlayActive) {
7✔
298
        return;
6✔
299
      }
300

301
      intervalRef.current = window.setInterval(() => {
1✔
302
        if (currentValue >= endDate) {
×
303
          clearInterval(intervalRef.current);
×
304
          setAutoPlayActive(false);
×
305
          return;
×
306
        }
307
        const newValue: Moment = currentValue.clone();
×
308

309
        if (_isFinite(playbackSpeed)) {
×
310
          wmsTimeHandler(
×
311
            moment(newValue.clone().add(playbackSpeed, 'hours').format())
312
          );
313
          setCurrentValue(
×
314
            moment(newValue.clone().add(playbackSpeed, 'hours').format())
315
          );
316
        } else {
317
          const time = moment(
×
318
            newValue
319
              .clone()
320
              .add(1, playbackSpeed as moment.unitOfTime.DurationConstructor)
321
              .format()
322
          );
323
          wmsTimeHandler(time);
×
324
          setCurrentValue(time);
×
325
        }
326
      }, 1000);
327

328
      return () => {
1✔
329
        if (intervalRef.current) {
1!
330
          clearInterval(intervalRef.current);
1✔
331
        }
332
      };
333
    }, [autoPlayActive, currentValue, endDate, playbackSpeed]);
334

335
    useEffect(() => {
8✔
336
      wrapTimeSlider();
3✔
337
      return () => {
3✔
338
        if (intervalRef.current !== null) {
3!
339
          clearInterval(intervalRef.current);
3✔
340
        }
341
      };
342
    }, [wrapTimeSlider]);
343

344
    useEffect(() => {
8✔
345
      if (autoPlayActive) {
4✔
346
        autoPlay();
1✔
347
      }
348
      return () => {
4✔
349
        if (intervalRef.current !== null) {
4!
350
          clearInterval(intervalRef.current);
4✔
351
        }
352
      };
353
    }, [autoPlayActive, autoPlay]);
354

355
    useEffect(() => {
8✔
356
      setStartDate(initStartDate);
3✔
357
      setEndDate(initEndDate);
3✔
358
    }, [initStartDate, initEndDate]);
359

360
    useEffect(() => {
8✔
361
      const prevProps = prevPropsRef.current;
3✔
362
      if (prevProps && prevProps.timeAwareLayers) {
3!
363
        prevProps.timeAwareLayers.forEach((pl, i) => {
×
364
          if (timeAwareLayers) {
×
365
            const tpl = timeAwareLayers[i];
×
366
            if (!_isEqual(getUid(pl), getUid(tpl))) {
×
367
              wrapTimeSlider();
×
368
              findRangeForLayers();
×
369
            }
370
          }
371
        });
372
      }
373

374
      prevPropsRef.current = { timeAwareLayers };
3✔
375
    }, [timeAwareLayers, findRangeForLayers, wrapTimeSlider]);
376

377
    useEffect(() => {
8✔
378
      timeSliderCustomHandler(value);
7✔
379
    }, [timeSliderCustomHandler, value]);
380

381
    useEffect(() => {
8✔
382
      setSliderToMostRecent();
3✔
383
    }, [setSliderToMostRecent]);
384

385
    useEffect(() => {
8✔
386
      if (autoPlayActive) {
4✔
387
        autoPlay();
1✔
388
      }
389
    }, [playbackSpeed, autoPlayActive, autoPlay]);
390

391
    const resetVisible = true;
8✔
392

393
    const startDateString = startDate.toISOString();
8✔
394
    const endDateString = endDate.toISOString();
8✔
395
    const valueString = currentValue.toISOString();
8✔
396
    const mid = startDate!.clone().add(endDate!.diff(startDate) / 2);
8✔
397
    const marks: Record<string, any> = {};
8✔
398
    const futureClass = moment().isBefore(value) ? ' timeslider-in-future' : '';
8!
399
    const extraCls = className ? className : '';
8!
400
    const disabledCls = timeAwareLayers.length < 1 ? 'no-layers-available' : '';
8✔
401

402
    marks[startDateString] = {
8✔
403
      label: startDate!.format(dateFormat)
404
    };
405
    marks[endDateString] = {
8✔
406
      label: endDate!.format(dateFormat),
407
      style: {
408
        left: 'unset',
409
        right: 0,
410
        transform: 'translate(50%)'
411
      }
412
    };
413
    marks[mid.toISOString()] = {
8✔
414
      label: mid.format(dateFormat)
415
    };
416

417
    const speedOptions = autoPlaySpeedOptions.map(function (val: number) {
8✔
418
      return (
56✔
419
        <Option
420
          key={val}
421
          value={val}
422
        >
423
          {val}
424
        </Option>
425
      );
426
    });
427

428
    return (
8✔
429
      <div className={`time-layer-slider ${disabledCls}`.trim()}>
430
        <Popover
431
          placement="topRight"
432
          title={tooltips.dataRange}
433
          trigger="click"
434
          content={
435
            <RangePicker
436
              showTime={{ format: 'HH:mm' }}
437
              defaultValue={[
438
                dayjs(startDate.toISOString()),
439
                dayjs(endDate.toISOString())
440
              ]}
441
              onOk={range => {
442
                if (!range) {
×
443
                  return;
×
444
                }
445
                const [start, end] = range;
×
446
                if (!start || !end) {
×
447
                  return;
×
448
                }
449

450
                updateDataRange([
×
451
                  moment(start.toISOString()),
452
                  moment(end.toISOString())
453
                ]);
454
              }}
455
            />
456
          }
457
        >
458
          <SimpleButton
459
            className="change-datarange-button"
460
            icon={<FontAwesomeIcon icon={faCalendar} />}
461
          />
462
        </Popover>
463
        {resetVisible ? (
8!
464
          <SimpleButton
465
            type="primary"
466
            icon={<FontAwesomeIcon icon={faSync} />}
467
            onClick={setSliderToMostRecent}
468
            tooltip={tooltips.setToMostRecent}
469
          />
470
        ) : null}
471
        <div className="time-slider-container">
472
          <TimeSlider
473
            className={`${extraCls} timeslider ${futureClass}`.trim()}
474
            formatString={dateFormat}
475
            defaultValue={startDateString}
476
            min={startDateString}
477
            max={endDateString}
478
            value={valueString}
479
            marks={marks}
480
            onChange={onTimeChanged}
481
          />
482
          <div className="spin-indicator">
483
            <Spin spinning={isLoading} size="small" />
484
          </div>
485
        </div>
486
        <div className="time-value">
487
          {currentValue.format(dateFormat || 'DD.MM.YYYY HH:mm:ss')}
8!
488
        </div>
489
        <ToggleButton
490
          type="primary"
491
          icon={<FontAwesomeIcon icon={faPlayCircle} />}
492
          className={extraCls + ' playback'}
493
          pressed={autoPlayActive}
494
          onChange={autoPlay}
495
          tooltip={autoPlayActive ? 'Pause' : 'Autoplay'}
8✔
496
          aria-label={autoPlayActive ? 'Pause' : 'Autoplay'}
8✔
497
          pressedIcon={<FontAwesomeIcon icon={faPauseCircle} />}
498
        />
499
        <Select
500
          defaultValue={'hours'}
501
          className={extraCls + ' speed-picker'}
502
          onChange={onPlaybackSpeedChange}
503
          popupMatchSelectWidth={false}
504
          dropdownStyle={{ minWidth: '100px' }}
505
        >
506
          {speedOptions}
507
          <Option value="hours">{tooltips.hours}</Option>
508
          <Option value="days">{tooltips.days}</Option>
509
          <Option value="weeks">{tooltips.weeks}</Option>
510
          <Option value="months">{tooltips.months}</Option>
511
          <Option value="years">{tooltips.years}</Option>
512
        </Select>
513
      </div>
514
    );
515
  },
516
  (prevProps, nextProps) => {
517
    if (!_isEqual(prevProps.value, nextProps.value)) {
×
518
      return false;
×
519
    }
520
    if (!_isEqual(prevProps.timeAwareLayers, nextProps.timeAwareLayers)) {
×
521
      return false;
×
522
    }
NEW
523
    return _isEqual(prevProps.tooltips, nextProps.tooltips);
×
524
  }
525
);
526

527
export default TimeLayerSliderPanel;
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

© 2026 Coveralls, Inc