Projekt

Obecné

Profil

Stáhnout (20.1 KB) Statistiky
| Větev: | Revize:
1
/* global L */
2
/* global $ */
3

    
4
var mymap
5
var heatmapLayer = null
6
var marksLayer = null
7

    
8
// values for arrow keys
9
const arrowKeyLEFT = 37
10
const arrowKeyRIGHT = 39
11

    
12
var startX = 49.7248
13
var startY = 13.3521
14
var startZoom = 17
15

    
16
var dataSourceRoute
17
let positionsSourceRoute
18

    
19
let currentTime
20

    
21
let currentDate
22

    
23
var timer
24
var isAnimationRunning = false
25
var data = []
26

    
27
var info = []
28
let currentInfo = 0
29

    
30
// dictionary for names of datasets
31
const datasetDictNameDisplayName = {}
32
var datasetSelected = []
33

    
34
// data only for one day
35
let lockedDay = false
36

    
37
// loading information for async operations
38
let loading = 0
39

    
40
// default loader showup delay
41
const defaultLoaderDelay = 1000
42

    
43
// marks for all datasets
44
const dataSourceMarks = {}
45

    
46
const globalMarkersHolder = {}
47
// all marker from which popup was removed
48
// contains: {key:[L.circle,L.pupup]}
49
// key: x and y, x + '' + y string
50
let globalMarkersChanged = {}
51

    
52
const loadingCallbackNested = (func, delay) => {
53
  setTimeout(() => {
54
    func(loading)
55
    if (loading) {
56
      loadingCallbackNested(func, delay)
57
    }
58
  }, delay)
59
}
60
const loadingY = (delay = defaultLoaderDelay) => {
61
  loading++
62
  // check after nms if there is something that is loading
63
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
64
}
65
const loadingN = (delay = defaultLoaderDelay) => {
66
  loading--
67
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
68
}
69

    
70
const changeCurrentTime = (time = null) => {
71
  if (time !== null) {
72
    currentTime = time
73
  } else {
74
    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
75
  }
76
}
77

    
78
const changeCurrentDate = (date = null) => {
79
  if (date) {
80
    currentDate = new Date(date)
81
  } else {
82
    currentDate = new Date($('#date').val())
83
  }
84

    
85
  $('#date').val(currentDateToString())
86
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
87

    
88
  data = []
89
}
90
const currentDayToString = () => {
91
  const day = currentDate.getDate()
92
  return day > 9 ? `${day}` : `0${day}`
93
}
94
const currentMonthToString = () => {
95
  const month = currentDate.getMonth() + 1
96
  return month > 9 ? `${month}` : `0${month}`
97
}
98
const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
99
const addDayToCurrentDate = (day) => {
100
  currentDate.setDate(currentDate.getDate() + day)
101
  changeCurrentDate(currentDate)
102
}
103
const toggleDayLock = () => {
104
  lockedDay = !lockedDay
105
  $('#player-date').toggleClass('lock')
106
}
107

    
108
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
109
  const headers = new Headers()
110
  const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, {
111
    method: 'GET',
112
    headers: headers
113
  })
114
  const beforeJson = await fetch(myRequest)
115
  return beforeJson.json()
116
}
117

    
118
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
119
  const headers = new Headers()
120
  const myRequest = new Request(positionRoute + '/' + datasetName, {
121
    method: 'GET',
122
    headers: headers
123
  })
124
  const beforeJson = await fetch(myRequest)
125
  return beforeJson.json()
126
}
127

    
128
const genPopUpControlButtons = (currentPage, numPages, onNextClick, onPreviousClick) => ({
129
  previousButton: '<button id="previous-info-btn" class="circle-button" onclick="previousInfo()"></button>',
130
  nextButton: '<button id="next-info-btn" class="circle-button next" onclick="nextInfo()"></button>',
131
  posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>`
132
})
133

    
134
const genPopUpControls = (controls) => {
135
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
136
}
137

    
138
const genMultipleDatasetsPopUp = (sum, currentPos, maxPos, datasetName) => {
139
  const popupHeader = `<strong id="dataset-info">${datasetName}</strong>`
140
  const popupData = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
141
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
142

    
143
  return `
144
  ${popupHeader}
145
  ${popupData}
146
  ${genPopUpControls([previousButton, posInfo, nextButton])}
147
  `
148
}
149

    
150
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
151
  autoPan: false,
152
  className: className
153
}).setLatLng([lat / num, lng / num])
154

    
155
const genPopUp = (datasetName, place, count, sum, currentPos, maxPos) => {
156
  const popupHeader = `
157
    <strong>${datasetName}</strong>
158
    <div id="place-info">${place}</div>`
159
  const popupData = `
160
    <div id="number-info">
161
      <span id="digit-info">${count}</span>
162
      <span id="total-info">${(sum && (sum != count)) ? '/' + sum : ''}</span>
163
    </div>`
164
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
165

    
166
  return `
167
  ${popupHeader}
168
  ${popupData}
169
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
170
  `
171
}
172

    
173
const onCheckboxClicked = async (checkbox) => {
174
  if ($(checkbox).prop('checked')) {
175
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
176
    changeUrl()
177
  } else {
178
    loadCheckboxDatasetNameData()
179

    
180
    data.forEach((item, index) => {
181
      Object.keys(item).forEach((datasetName) => {
182
        if (datasetName === $(checkbox).val()) {
183
          delete data[index][datasetName]
184
        }
185
      })
186
      drawHeatmap(data[currentTime])
187
    })
188

    
189
    changeUrl()
190
  }
191
}
192

    
193
const onArrowLeftRightKeysDownRegister = () => {
194
  $(document).keydown(function (e) {
195
    const { which } = e
196
    
197
    if (which === arrowKeyLEFT) {
198
      previous()
199
      e.preventDefault()
200
    } else if (which === arrowKeyRIGHT) {
201
      next()
202
      e.preventDefault()
203
    }
204
  })
205
}
206

    
207
const debounce = (func, delay) => {
208
  let inDebounce
209
  return function () {
210
    const context = this
211
    const args = arguments
212
    clearTimeout(inDebounce)
213
    inDebounce = setTimeout(() => func.apply(context, args), delay)
214
  }
215
}
216

    
217
const onValueChangeRegister = () => {
218
  $('#date').change(function () {
219
    changeCurrentDate($(this).val())
220
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
221
    changeUrl()
222
  })
223

    
224
  $('#dropdown-time input[type="radio"]').each(function () {
225
    $(this).change(function () {
226
      changeHour(parseInt($(this).val()))
227
      drawHeatmap(data[currentTime])
228
    })
229
  })
230

    
231
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
232
    $(this).change(
233
      debounce(() => onCheckboxClicked(this), 1000)
234
    )
235
  })
236
}
237

    
238
/**
239
 * Initialize leaflet map on start position which can be default or set based on user action
240
 */
241
// eslint-disable-next-line no-unused-vars
242
function initMap () {
243
  startX = localStorage.getItem('lat') || startX
244
  startY = localStorage.getItem('lng') || startY
245
  startZoom = localStorage.getItem('zoom') || startZoom
246

    
247
  mymap = L.map('heatmap').setView([startX, startY], startZoom)
248

    
249
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
250
    attribution: '',
251
    maxZoom: 19
252
  }).addTo(mymap)
253

    
254
  mymap.on('click', showInfo)
255
}
256

    
257
const getInfoLength = () => {
258
  const infoKeys = Object.keys(info)
259
  if (infoKeys.length === 1) {
260
    // return number of records in one dataset (one dataset in area)
261
    return info[infoKeys[0]].items.length
262
  }
263
  // return number of datasets (agregation of all datasets in area)
264
  return infoKeys.length
265
}
266

    
267
const getElFromObjectInfo = (position) => {
268
  const keys = Object.keys(info)
269
  return info[keys[position]]
270
}
271

    
272
const hasInfoMultipleDatasets = () => {
273
  return Object.keys(info).length > 1
274
}
275

    
276
function showInfo (e) {
277
  info = []
278
  currentInfo = 0
279

    
280
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
281
  // Todo change to variable - it is used in heatmap init
282
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
283
  const radius = 25 * stile / 256
284

    
285
  let i = 0
286
  let lat = 0
287
  let lng = 0
288

    
289
  let total = 0
290

    
291
  const datasetsInRadius = {}
292

    
293
  Object.keys(data[currentTime]).forEach((key) => {
294
    const namedData = data[currentTime][key]
295
    namedData.items.forEach(element => {
296
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
297
        lat += element.x
298
        lng += element.y
299
        info[i] = { place: element.place, number: element.number, datasetName: key }
300
        total += parseInt(element.number)
301
        i++
302
        datasetsInRadius[key] = true
303
      }
304
    })
305
  })
306

    
307
  // Process info for more then one dataset
308

    
309
  info = info.reduce((acc, item) => {
310
    if (!acc[item.datasetName]) {
311
      acc[item.datasetName] = {
312
        items: [],
313
        number: 0,
314
        datasetName: item.datasetName
315
      }
316
    }
317

    
318
    acc[item.datasetName].items.push(item)
319
    acc[item.datasetName].number += Number(item.number)
320
    return acc
321
  }, {})
322

    
323
  // There is one dataset
324

    
325
  const numDatasets = Object.keys(datasetsInRadius).length
326

    
327
  if (!numDatasets) {
328
    return
329
  }
330

    
331
  if (numDatasets === 1) {
332
    const infoDict = getElFromObjectInfo(0)
333
    const info_ = infoDict.items
334
    const { place, number } = info_[currentInfo]
335
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
336
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
337
      .openOn(mymap)
338

    
339
    if (info_.length === 1) {
340
      $('#previous-info-btn').prop('disabled', true)
341
      $('#next-info-btn').prop('disabled', true)
342
      $('.popup-controls').hide()
343
    }
344
  } else {
345
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
346

    
347
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
348
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
349
      .openOn(mymap)
350
  }
351
}
352

    
353
// eslint-disable-next-line no-unused-vars
354
function previousInfo () {
355
  const infoLength = getInfoLength()
356
  const previousCurrentInfo = currentInfo
357

    
358
  currentInfo = (currentInfo + infoLength - 1) % infoLength
359
  displayInfoText(previousCurrentInfo)
360
}
361

    
362
// eslint-disable-next-line no-unused-vars
363
function nextInfo () {
364
  const infoLength = getInfoLength()
365
  const previousCurrentInfo = currentInfo
366

    
367
  currentInfo = (currentInfo + 1) % infoLength
368
  displayInfoText(previousCurrentInfo)
369
}
370

    
371
function displayInfoText (previousInfoNum) {
372
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
373
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
374
  const infoLength = getInfoLength()
375
  const datasetInfo = $('#dataset-info')
376

    
377
  if (datasetInfo) {
378
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
379
  }
380

    
381
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
382
  $('#digit-info').html(info_.number)
383
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
384

    
385
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
386
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
387
}
388

    
389
// eslint-disable-next-line no-unused-vars
390
function setMapView (latitude, longitude, zoom) {
391
  localStorage.setItem('lat', latitude)
392
  localStorage.setItem('lng', longitude)
393
  localStorage.setItem('zoom', zoom)
394
  mymap.setView([latitude, longitude], zoom)
395
}
396

    
397
/**
398
 * Change animation start from playing to stopped or the other way round
399
 */
400
// eslint-disable-next-line no-unused-vars
401
function changeAnimationState () {
402
  isAnimationRunning = !isAnimationRunning
403

    
404
  if (isAnimationRunning) {
405
    $('#animate-btn').removeClass('play').addClass('pause')
406
    timer = setInterval(function () { next() }, 800)
407
  } else {
408
    clearTimeout(timer)
409
    $('#animate-btn').removeClass('pause').addClass('play')
410
  }
411
}
412

    
413
// eslint-disable-next-line no-unused-vars
414
async function previous () {
415
  if (loading) {
416
    return
417
  }
418

    
419
  currentTime = (currentTime + 23) % 24
420
  changeHour(currentTime)
421
  mymap.closePopup()
422
  if (!lockedDay && (currentTime === 23)) {
423
    addDayToCurrentDate(-1)
424
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
425
  } else {
426
    drawHeatmap(data[currentTime])
427
  }
428
}
429

    
430
async function next () {
431
  if (loading) {
432
    return
433
  }
434

    
435
  currentTime = (currentTime + 1) % 24
436
  changeHour(currentTime)
437
  mymap.closePopup()
438
  if (!lockedDay && (currentTime === 0)) {
439
    addDayToCurrentDate(1)
440
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
441
  } else {
442
    drawHeatmap(data[currentTime])
443
  }
444
}
445

    
446
/**
447
 * Change browser url based on animation step.
448
 */
449
function changeUrl () {
450
  window.history.pushState(
451
    '',
452
    document.title,
453
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type=' + current, '')}`
454
  )
455
}
456

    
457
function updateHeaderControls () {
458
  $(`#time_${currentTime}`).prop('checked', true)
459
  $('#dropdownMenuButtonTime').html((currentTime < 10 ? '0' : '') + `${currentTime}:00`)
460
}
461

    
462
function setTimeline () {
463
  $('#player-time > span').text(currentTime + ':00')
464
  $('#player-time').attr('class', 'time hour-' + currentTime)
465
}
466

    
467
const loadingTimeline = (isLoading) => {
468
  if (isLoading) {
469
    loadingYTimeline()
470
  } else {
471
    loadingNTimeline()
472
  }
473
}
474
const loadingYTimeline = () => {
475
  $('#player-time > .spinner-border').removeClass('d-none')
476
  $('#player-time > span').text('')
477
}
478
const loadingNTimeline = () => {
479
  $('#player-time > .spinner-border').addClass('d-none')
480
  setTimeline()
481
}
482
const onChangeHour = (hour) => {
483
  changeHour(hour)
484
  drawHeatmap(data[currentTime])
485
}
486

    
487
const changeHour = (hour) => {
488
  $('#player-time').removeAttr('style')
489
  changeCurrentTime(hour)
490
  updateHeaderControls()
491
  setTimeline()
492
  changeUrl()
493
}
494

    
495
/**
496
 * Load and display heatmap layer for current data
497
 * @param {string} opendataRoute route to dataset source
498
 * @param {string} positionsRoute  route to dataset postitions source
499
 */
500
// eslint-disable-next-line no-unused-vars
501
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
502
  loadCheckboxDatasetNameData()
503

    
504
  dataSourceRoute = opendataRoute
505
  positionsSourceRoute = positionsRoute
506
  const allPromises = []
507
  data[currentTime] = {}
508

    
509
  const dataSelectedHandler = async (datasetName) => {
510
    if (!(datasetName in dataSourceMarks)) {
511
      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
512
    }
513
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
514
    data[currentTime][datasetName] = datasetData
515
  }
516
  datasetSelected.forEach((datasetName) => {
517
    allPromises.push(dataSelectedHandler(datasetName))
518
  })
519

    
520
  loadingY(loaderDelay)
521
  Promise.all(allPromises).then(
522
    () => {
523
      loadingN(0)
524
      drawDataSourceMarks(dataSourceMarks)
525
      drawHeatmap(data[currentTime])
526
      preload(currentTime, 1, currentDateToString())
527
      preload(currentTime, -1, currentDateToString())
528
    }
529
  )
530
}
531

    
532
function drawDataSourceMarks (data) {
533
  if (marksLayer != null) {
534
    mymap.removeLayer(marksLayer)
535
  }
536

    
537
  marksLayer = L.layerGroup()
538

    
539
  Object.keys(data).forEach((key_) => {
540
    for (var key in data[key_]) {
541
      const { x, y, name } = data[key_][key]
542
      const pop =
543
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
544
            .setContent(genPopUp(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
545
      const newCircle =
546
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
547
          .bindPopup(pop)
548
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
549
      marksLayer.addLayer(
550
        newCircle
551
      )
552
    }
553
  })
554

    
555
  marksLayer.setZIndex(-1).addTo(mymap)
556
}
557

    
558
async function preload (time, change, date) {
559
  loadingY()
560
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
561
    if (!data[nTime]) {
562
      data[nTime] = {}
563
    }
564

    
565
    datasetSelected.forEach(async (datasetName) => {
566
      if (!data[nTime][datasetName]) {
567
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
568
      }
569
    })
570
  }
571
  loadingN()
572
}
573

    
574
function drawHeatmap (dataRaw) {
575
  // Todo still switched
576
  const dataDict = dataRaw
577
  const mergedPoints = []
578
  let max = 0
579

    
580
  if (Object.keys(globalMarkersChanged).length) {
581
    Object.keys(globalMarkersChanged).forEach(function (key) {
582
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
583
    })
584
    globalMarkersChanged = {}
585
  }
586

    
587
  Object.keys(dataDict).forEach((key) => {
588
    const data = dataDict[key]
589
    max = Math.max(max, data.max)
590

    
591
    if (data != null) {
592
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
593
      const points = data.items.map((point) => {
594
        const { x, y, number } = point
595
        const key = x + '' + y
596
        const holder = globalMarkersHolder[key]
597

    
598
        if (!globalMarkersChanged[key] && number) {
599
        // There is data for this marker => unbind popup with zero value
600
          holder[0] = holder[0].unbindPopup()
601
          globalMarkersChanged[key] = holder
602
        }
603

    
604
        return [x, y, number]
605
      })
606
      mergedPoints.push(...points)
607
    } else {
608
      if (heatmapLayer != null) {
609
        mymap.removeLayer(heatmapLayer)
610
      }
611
    }
612
  })
613

    
614
  if (heatmapLayer != null) {
615
    mymap.removeLayer(heatmapLayer)
616
  }
617

    
618
  if (mergedPoints.length) {
619
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
620
  }
621
}
622

    
623
/**
624
 * Checks dataset availibility
625
 * @param {string} route authority for datasets availibility checks
626
 */
627
// eslint-disable-next-line no-unused-vars
628
function checkDataSetsAvailability (route) {
629
  $.ajax({
630
    type: 'POST',
631
    // Todo it might be good idea to change db collections format
632
    url: route + '/' + currentDateToString(),
633
    success: function (result) {
634
      updateAvailableDataSets(result)
635
    }
636
  })
637
}
638

    
639
function updateAvailableDataSets (available) {
640
  let leastOneOptionEnabled = false
641

    
642
  $('#dropdown-dataset .dropdown-item').each(function () {
643
    const input = $(this).find('input')
644
    const inputVal = input[0].value
645

    
646
    if (!(inputVal in available)) {
647
      $(this).addClass('disabled')
648
      $(input).prop('checked', false)
649
    } else {
650
      leastOneOptionEnabled = true
651
      $(this).removeClass('disabled')
652
    }
653
  })
654

    
655
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
656
}
657

    
658
function formatDate (date) {
659
  var day = String(date.getDate())
660
  var month = String(date.getMonth() + 1)
661

    
662
  if (day.length === 1) {
663
    day = '0' + day
664
  }
665

    
666
  if (month.length === 1) {
667
    month = '0' + month
668
  }
669

    
670
  return date.getFullYear() + '-' + month + '-' + day
671
}
672

    
673
// eslint-disable-next-line no-unused-vars
674
function initDatepicker (availableDatesSource) {
675
  var availableDates = ''
676

    
677
  $.ajax({
678
    type: 'GET',
679
    url: availableDatesSource,
680
    success: function (result) {
681
      availableDates = String(result).split(',')
682
    }
683
  }).then(function () {
684
    $('#date').datepicker({
685
      format: 'yyyy-mm-dd',
686
      language: 'cs',
687
      beforeShowDay: function (date) {
688
        if (availableDates.indexOf(formatDate(date)) < 0) {
689
          return { enabled: false, tooltip: 'Žádná data' }
690
        } else {
691
          return { enabled: true }
692
        }
693
      },
694
      autoclose: true
695
    })
696
  })
697
}
698

    
699
function initLocationsMenu () {
700
  var locationsWrapper = '.locations'
701
  var locationsDisplayClass = 'show'
702

    
703
  if ($(window).width() <= 480) {
704
    $(locationsWrapper).removeClass(locationsDisplayClass)
705
  } else {
706
    $(locationsWrapper).addClass(locationsDisplayClass)
707
  }
708
}
709

    
710
function onDocumentReady () {
711
  $('#dropdown-dataset').on('click', function (e) {
712
    e.stopPropagation()
713
  })
714

    
715
  $('#btn-update-heatmap').prop('name', '')
716
  changeCurrentTime()
717
  changeCurrentDate()
718
  onValueChangeRegister()
719
  onArrowLeftRightKeysDownRegister()
720
}
721

    
722
const loadCheckboxDatasetNameData = () => {
723
  datasetSelected = []
724
  $('#dropdown-dataset .dropdown-item').each(function () {
725
    const input = $(this).find('input')
726
    const inputVal = input[0].value
727

    
728
    if (input[0].checked) {
729
      datasetSelected.push(inputVal)
730
    }
731

    
732
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
733
  })
734
}
735

    
736
function dragTimeline () {
737
  const hourElemWidthPx = 26
738

    
739
  const elem = $('#player-time')
740
  const offset = elem.offset().left - elem.parent().offset().left
741

    
742
  if (offset >= 0 && offset <= elem.parent().width()) {
743
    const hour = Math.round(offset / hourElemWidthPx)
744

    
745
    if (hour != currentTime) {
746
      elem.attr('class', 'time hour-' + hour)
747
      $('#player-time span').html(hour + ':00')
748

    
749
      onChangeHour(hour)
750
    }
751
  }
752
}
(2-2/2)