Projekt

Obecné

Profil

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

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

    
8
var startX = 49.7248
9
var startY = 13.3521
10
var startZoom = 17
11

    
12
var dataSourceRoute
13
let positionsSourceRoute
14

    
15
let currentTime
16

    
17
let currentDate
18

    
19
var timer
20
var isAnimationRunning = false
21
var data = []
22

    
23
var info = []
24
let currentInfo = 0
25

    
26
// dictionary for names of datasets
27
const datasetDictNameDisplayName = {}
28
var datasetSelected = []
29

    
30
// data only for one day
31
let lockedDay = false
32

    
33
// loading information for async operations
34
let loading = 0
35

    
36
// default loader showup delay
37
const defaultLoaderDelay = 1000
38

    
39
// marks for all datasets
40
const dataSourceMarks = {}
41

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

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

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

    
74
const changeCurrentDate = (date = null) => {
75
  if (date) {
76
    currentDate = new Date(date)
77
  } else {
78
    currentDate = new Date($('#date').val())
79
  }
80

    
81
  $('#date').val(currentDateToString())
82
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
83

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

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

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

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

    
130
const genPopUpControls = (controls) => {
131
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
132
}
133

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

    
139
  return `
140
  ${popupHeader}
141
  ${popupData}
142
  ${genPopUpControls([previousButton, posInfo, nextButton])}
143
  `
144
}
145

    
146
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
147
  autoPan: false,
148
  className: className
149
}).setLatLng([lat / num, lng / num])
150

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

    
162
  return `
163
  ${popupHeader}
164
  ${popupData}
165
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
166
  `
167
}
168

    
169
const onCheckboxClicked = async (checkbox) => {
170
  if ($(checkbox).prop('checked')) {
171
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
172
    changeUrl()
173
  } else {
174
    loadCheckboxDatasetNameData()
175

    
176
    data.forEach((item, index) => {
177
      Object.keys(item).forEach((datasetName) => {
178
        if (datasetName === $(checkbox).val()) {
179
          delete data[index][datasetName]
180
        }
181
      })
182
      drawHeatmap(data[currentTime])
183
    })
184

    
185
    changeUrl()
186
  }
187
}
188

    
189
const debounce = (func, delay) => {
190
  let inDebounce
191
  return function () {
192
    const context = this
193
    const args = arguments
194
    clearTimeout(inDebounce)
195
    inDebounce = setTimeout(() => func.apply(context, args), delay)
196
  }
197
}
198

    
199
const onValueChangeRegister = () => {
200
  $('#date').change(function () {
201
    changeCurrentDate($(this).val())
202
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
203
    changeUrl()
204
  })
205

    
206
  $('#dropdown-time input[type="radio"]').each(function () {
207
    $(this).change(function () {
208
      changeHour(parseInt($(this).val()))
209
      drawHeatmap(data[currentTime])
210
    })
211
  })
212

    
213
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
214
    $(this).change(
215
      debounce(() => onCheckboxClicked(this), 1000)
216
    )
217
  })
218
}
219

    
220
/**
221
 * Initialize leaflet map on start position which can be default or set based on user action
222
 */
223
// eslint-disable-next-line no-unused-vars
224
function initMap () {
225
  startX = localStorage.getItem('lat') || startX
226
  startY = localStorage.getItem('lng') || startY
227
  startZoom = localStorage.getItem('zoom') || startZoom
228

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

    
231
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
232
    attribution: '',
233
    maxZoom: 19
234
  }).addTo(mymap)
235

    
236
  mymap.on('click', showInfo)
237
}
238

    
239
const getInfoLength = () => {
240
  const infoKeys = Object.keys(info)
241
  if (infoKeys.length === 1) {
242
    // return number of records in one dataset (one dataset in area)
243
    return info[infoKeys[0]].items.length
244
  }
245
  // return number of datasets (agregation of all datasets in area)
246
  return infoKeys.length
247
}
248

    
249
const getElFromObjectInfo = (position) => {
250
  const keys = Object.keys(info)
251
  return info[keys[position]]
252
}
253

    
254
const hasInfoMultipleDatasets = () => {
255
  return Object.keys(info).length > 1
256
}
257

    
258
function showInfo (e) {
259
  info = []
260
  currentInfo = 0
261

    
262
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
263
  // Todo change to variable - it is used in heatmap init
264
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
265
  const radius = 25 * stile / 256
266

    
267
  let i = 0
268
  let lat = 0
269
  let lng = 0
270

    
271
  let total = 0
272

    
273
  const datasetsInRadius = {}
274

    
275
  Object.keys(data[currentTime]).forEach((key) => {
276
    const namedData = data[currentTime][key]
277
    namedData.items.forEach(element => {
278
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
279
        lat += element.x
280
        lng += element.y
281
        info[i] = { place: element.place, number: element.number, datasetName: key }
282
        total += parseInt(element.number)
283
        i++
284
        datasetsInRadius[key] = true
285
      }
286
    })
287
  })
288

    
289
  // Process info for more then one dataset
290

    
291
  info = info.reduce((acc, item) => {
292
    if (!acc[item.datasetName]) {
293
      acc[item.datasetName] = {
294
        items: [],
295
        number: 0,
296
        datasetName: item.datasetName
297
      }
298
    }
299

    
300
    acc[item.datasetName].items.push(item)
301
    acc[item.datasetName].number += Number(item.number)
302
    return acc
303
  }, {})
304

    
305
  // There is one dataset
306

    
307
  const numDatasets = Object.keys(datasetsInRadius).length
308

    
309
  if (!numDatasets) {
310
    return
311
  }
312

    
313
  if (numDatasets === 1) {
314
    const infoDict = getElFromObjectInfo(0)
315
    const info_ = infoDict.items
316
    const { place, number } = info_[currentInfo]
317
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
318
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
319
      .openOn(mymap)
320

    
321
    if (info_.length === 1) {
322
      $('#previous-info-btn').prop('disabled', true)
323
      $('#next-info-btn').prop('disabled', true)
324
      $('.popup-controls').hide()
325
    }
326
  } else {
327
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
328

    
329
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
330
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
331
      .openOn(mymap)
332
  }
333
}
334

    
335
// eslint-disable-next-line no-unused-vars
336
function previousInfo () {
337
  const infoLength = getInfoLength()
338
  const previousCurrentInfo = currentInfo
339

    
340
  currentInfo = (currentInfo + infoLength - 1) % infoLength
341
  displayInfoText(previousCurrentInfo)
342
}
343

    
344
// eslint-disable-next-line no-unused-vars
345
function nextInfo () {
346
  const infoLength = getInfoLength()
347
  const previousCurrentInfo = currentInfo
348

    
349
  currentInfo = (currentInfo + 1) % infoLength
350
  displayInfoText(previousCurrentInfo)
351
}
352

    
353
function displayInfoText (previousInfoNum) {
354
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
355
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
356
  const infoLength = getInfoLength()
357
  const datasetInfo = $('#dataset-info')
358

    
359
  if (datasetInfo) {
360
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
361
  }
362

    
363
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
364
  $('#digit-info').html(info_.number)
365
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
366

    
367
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
368
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
369
}
370

    
371
// eslint-disable-next-line no-unused-vars
372
function setMapView (latitude, longitude, zoom) {
373
  localStorage.setItem('lat', latitude)
374
  localStorage.setItem('lng', longitude)
375
  localStorage.setItem('zoom', zoom)
376
  mymap.setView([latitude, longitude], zoom)
377
}
378

    
379
/**
380
 * Change animation start from playing to stopped or the other way round
381
 */
382
// eslint-disable-next-line no-unused-vars
383
function changeAnimationState () {
384
  isAnimationRunning = !isAnimationRunning
385

    
386
  if (isAnimationRunning) {
387
    $('#animate-btn').removeClass('play').addClass('pause')
388
    timer = setInterval(function () { next() }, 800)
389
  } else {
390
    clearTimeout(timer)
391
    $('#animate-btn').removeClass('pause').addClass('play')
392
  }
393
}
394

    
395
// eslint-disable-next-line no-unused-vars
396
async function previous () {
397
  if (loading) {
398
    return
399
  }
400

    
401
  $("#player-time").removeAttr("style");
402

    
403
  currentTime = (currentTime + 23) % 24
404
  changeHour(currentTime)
405
  mymap.closePopup()
406
  if (!lockedDay && (currentTime === 23)) {
407
    addDayToCurrentDate(-1)
408
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
409
  } else {
410
    drawHeatmap(data[currentTime])
411
  }
412
}
413

    
414
async function next () {
415
  if (loading) {
416
    return
417
  }
418

    
419
  $("#player-time").removeAttr("style");
420
  
421
  currentTime = (currentTime + 1) % 24
422
  changeHour(currentTime)
423
  mymap.closePopup()
424
  if (!lockedDay && (currentTime === 0)) {
425
    addDayToCurrentDate(1)
426
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
427
  } else {
428
    drawHeatmap(data[currentTime])
429
  }
430
}
431

    
432
/**
433
 * Change browser url based on animation step.
434
 */
435
function changeUrl () {
436
  window.history.pushState(
437
    '',
438
    document.title,
439
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type=' + current, '')}`
440
  )
441
}
442

    
443
function updateHeaderControls () {
444
  $(`#time_${currentTime}`).prop('checked', true)
445
  $('#dropdownMenuButtonTime').html((currentTime < 10 ? '0' : '') + `${currentTime}:00`)
446
}
447

    
448
function setTimeline () {
449
  $('#player-time > span').text(currentTime + ':00')
450
  $('#player-time').attr('class', 'time hour-' + currentTime)
451
}
452
const loadingTimeline = (isLoading) => {
453
  if (isLoading) {
454
    loadingYTimeline()
455
  } else {
456
    loadingNTimeline()
457
  }
458
}
459
const loadingYTimeline = () => {
460
  $('#player-time > .spinner-border').removeClass('d-none')
461
  $('#player-time > span').text('')
462
}
463
const loadingNTimeline = () => {
464
  $('#player-time > .spinner-border').addClass('d-none')
465
  setTimeline()
466
}
467
const onChangeHour = (hour) => {
468
  changeHour(hour)
469
  drawHeatmap(data[currentTime])
470
}
471

    
472
const changeHour = (hour) => {
473
  changeCurrentTime(hour)
474
  updateHeaderControls()
475
  setTimeline()
476
  changeUrl()
477
}
478

    
479
/**
480
 * Load and display heatmap layer for current data
481
 * @param {string} opendataRoute route to dataset source
482
 * @param {string} positionsRoute  route to dataset postitions source
483
 */
484
// eslint-disable-next-line no-unused-vars
485
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
486
  loadCheckboxDatasetNameData()
487

    
488
  dataSourceRoute = opendataRoute
489
  positionsSourceRoute = positionsRoute
490
  const allPromises = []
491
  data[currentTime] = {}
492

    
493
  const dataSelectedHandler = async (datasetName) => {
494
    if (!(datasetName in dataSourceMarks)) {
495
      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
496
    }
497
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
498
    data[currentTime][datasetName] = datasetData
499
  }
500
  datasetSelected.forEach((datasetName) => {
501
    allPromises.push(dataSelectedHandler(datasetName))
502
  })
503

    
504
  loadingY(loaderDelay)
505
  Promise.all(allPromises).then(
506
    () => {
507
      loadingN(0)
508
      drawDataSourceMarks(dataSourceMarks)
509
      drawHeatmap(data[currentTime])
510
      preload(currentTime, 1, currentDateToString())
511
      preload(currentTime, -1, currentDateToString())
512
    }
513
  )
514
}
515

    
516
function drawDataSourceMarks (data) {
517
  if (marksLayer != null) {
518
    mymap.removeLayer(marksLayer)
519
  }
520

    
521
  marksLayer = L.layerGroup()
522

    
523
  Object.keys(data).forEach((key_) => {
524
    for (var key in data[key_]) {
525
      const { x, y, name } = data[key_][key]
526
      const pop =
527
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
528
            .setContent(genPopUp(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
529
      const newCircle =
530
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
531
          .bindPopup(pop)
532
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
533
      marksLayer.addLayer(
534
        newCircle
535
      )
536
    }
537
  })
538

    
539
  marksLayer.setZIndex(-1).addTo(mymap)
540
}
541

    
542
async function preload (time, change, date) {
543
  loadingY()
544
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
545
    if (!data[nTime]) {
546
      data[nTime] = {}
547
    }
548

    
549
    datasetSelected.forEach(async (datasetName) => {
550
      if (!data[nTime][datasetName]) {
551
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
552
      }
553
    })
554
  }
555
  loadingN()
556
}
557

    
558
function drawHeatmap (dataRaw) {
559
  // Todo still switched
560
  const dataDict = dataRaw
561
  const mergedPoints = []
562
  let max = 0
563

    
564
  if (Object.keys(globalMarkersChanged).length) {
565
    Object.keys(globalMarkersChanged).forEach(function (key) {
566
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
567
    })
568
    globalMarkersChanged = {}
569
  }
570

    
571
  Object.keys(dataDict).forEach((key) => {
572
    const data = dataDict[key]
573
    max = Math.max(max, data.max)
574

    
575
    if (data != null) {
576
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
577
      const points = data.items.map((point) => {
578
        const { x, y, number } = point
579
        const key = x + '' + y
580
        const holder = globalMarkersHolder[key]
581

    
582
        if (!globalMarkersChanged[key] && number) {
583
        // There is data for this marker => unbind popup with zero value
584
          holder[0] = holder[0].unbindPopup()
585
          globalMarkersChanged[key] = holder
586
        }
587

    
588
        return [x, y, number]
589
      })
590
      mergedPoints.push(...points)
591
    } else {
592
      if (heatmapLayer != null) {
593
        mymap.removeLayer(heatmapLayer)
594
      }
595
    }
596
  })
597

    
598
  if (heatmapLayer != null) {
599
    mymap.removeLayer(heatmapLayer)
600
  }
601

    
602
  if (mergedPoints.length) {
603
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
604
  }
605
}
606

    
607
/**
608
 * Checks dataset availibility
609
 * @param {string} route authority for datasets availibility checks
610
 */
611
// eslint-disable-next-line no-unused-vars
612
function checkDataSetsAvailability (route) {
613
  $.ajax({
614
    type: 'POST',
615
    // Todo it might be good idea to change db collections format
616
    url: route + '/' + currentDateToString(),
617
    success: function (result) {
618
      updateAvailableDataSets(result)
619
    }
620
  })
621
}
622

    
623
function updateAvailableDataSets (available) {
624
  let leastOneOptionEnabled = false
625

    
626
  $('#dropdown-dataset .dropdown-item').each(function () {
627
    const input = $(this).find('input')
628
    const inputVal = input[0].value
629

    
630
    if (!(inputVal in available)) {
631
      $(this).addClass('disabled')
632
      $(input).prop('checked', false)
633
    } else {
634
      leastOneOptionEnabled = true
635
      $(this).removeClass('disabled')
636
    }
637
  })
638

    
639
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
640
}
641

    
642
function formatDate (date) {
643
  var day = String(date.getDate())
644
  var month = String(date.getMonth() + 1)
645

    
646
  if (day.length === 1) {
647
    day = '0' + day
648
  }
649

    
650
  if (month.length === 1) {
651
    month = '0' + month
652
  }
653

    
654
  return date.getFullYear() + '-' + month + '-' + day
655
}
656

    
657
// eslint-disable-next-line no-unused-vars
658
function initDatepicker (availableDatesSource) {
659
  var availableDates = ''
660

    
661
  $.ajax({
662
    type: 'GET',
663
    url: availableDatesSource,
664
    success: function (result) {
665
      availableDates = String(result).split(',')
666
    }
667
  }).then(function () {
668
    $('#date').datepicker({
669
      format: 'yyyy-mm-dd',
670
      language: 'cs',
671
      beforeShowDay: function (date) {
672
        if (availableDates.indexOf(formatDate(date)) < 0) {
673
          return { enabled: false, tooltip: 'Žádná data' }
674
        } else {
675
          return { enabled: true }
676
        }
677
      },
678
      autoclose: true
679
    })
680
  })
681
}
682

    
683
function initLocationsMenu () {
684
  var locationsWrapper = '.locations'
685
  var locationsDisplayClass = 'show'
686

    
687
  if ($(window).width() <= 480) {
688
    $(locationsWrapper).removeClass(locationsDisplayClass)
689
  } else {
690
    $(locationsWrapper).addClass(locationsDisplayClass)
691
  }
692
}
693

    
694
function onDocumentReady () {
695
  $('#dropdown-dataset').on('click', function (e) {
696
    e.stopPropagation()
697
  })
698

    
699
  $('#btn-update-heatmap').prop('name', '')
700
  changeCurrentTime()
701
  changeCurrentDate()
702
  onValueChangeRegister()
703
}
704

    
705
const loadCheckboxDatasetNameData = () => {
706
  datasetSelected = []
707
  $('#dropdown-dataset .dropdown-item').each(function () {
708
    const input = $(this).find('input')
709
    const inputVal = input[0].value
710

    
711
    if (input[0].checked) {
712
      datasetSelected.push(inputVal)
713
    }
714

    
715
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
716
  })
717
}
718

    
719
function dragTimeline() {
720
  let hourElemWidthPx = 26
721

    
722
  let elem = $("#player-time")
723
  let offset = elem.offset().left - elem.parent().offset().left
724

    
725
  elem.draggable({ containment: "parent", axis: "x", cursor: "ew-resize" })
726

    
727
  if (offset >= 0 && offset <= elem.parent().width()) {
728
    let hour = Math.round(offset / hourElemWidthPx)
729
    
730
    elem.attr("class", "time hour-" + hour)
731
    $("#player-time span").html(hour + ":00")
732

    
733
    onChangeHour(hour)
734
  }
735
}
(2-2/2)