Projekt

Obecné

Profil

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

    
4
var mymap
5
var heatmapLayer = null
6
var markersLayer = 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
//
28
// info = {
29
//  DATASETNAME: {
30
//    items: Array,
31
//    number: Number,
32
//    datasetName: String
33
// }
34
// }
35
//
36
var info = []
37
let currentPageInPopup = 0
38

    
39
// dictionary for names of datasets
40
const datasetDictNameDisplayName = {}
41
var datasetSelected = []
42

    
43
// data only for one day
44
let lockedDay = false
45

    
46
// loading information for async operations
47
let loading = 0
48

    
49
// default loader showup delay
50
const defaultLoaderDelay = 1000
51

    
52
// marks for all datasets
53
const dataSourceMarks = {}
54

    
55
const globalMarkersHolder = {}
56
// all marker from which popup was removed
57
// contains: {key:[L.circle,L.pupup]}
58
// key: x and y, x + '' + y string
59
let globalMarkersChanged = {}
60

    
61
const globalPopup = {
62
  coord: {
63
    lat: 0,
64
    lng: 0
65
  },
66
  _popup: null
67
}
68

    
69

    
70

    
71

    
72
function onDocumentReady () {
73
  $('#dropdown-dataset').on('click', function (e) {
74
    e.stopPropagation()
75
  })
76

    
77
  $('#btn-update-heatmap').prop('name', '')
78
  changeCurrentTime()
79
  changeCurrentDate()
80
  onValueChangeRegister()
81
  onArrowLeftRightKeysDownRegister()
82
}
83

    
84

    
85

    
86

    
87
/* ------------ DATA FETCHERS ------------ */
88

    
89
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
90
  const headers = new Headers()
91
  const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, {
92
    method: 'GET',
93
    headers: headers
94
  })
95
  const beforeJson = await fetch(myRequest)
96

    
97
  return beforeJson.json()
98
}
99

    
100
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
101
  const headers = new Headers()
102
  const myRequest = new Request(positionRoute + '/' + datasetName, {
103
    method: 'GET',
104
    headers: headers
105
  })
106
  const beforeJson = await fetch(myRequest)
107

    
108
  return beforeJson.json()
109
}
110

    
111
async function preload (time, change, date) {
112
  loadingY()
113

    
114
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
115
    if (!data[nTime]) {
116
      data[nTime] = {}
117
    }
118

    
119
    datasetSelected.forEach(async (datasetName) => {
120
      if (!data[nTime][datasetName]) {
121
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
122
      }
123
    })
124
  }
125

    
126
  loadingN()
127
}
128

    
129
/**
130
 * Load and display heatmap layer for current data
131
 * 
132
 * @param {string} opendataRoute route to dataset source
133
 * @param {string} positionsRoute  route to dataset postitions source
134
 */
135
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
136
  loadCheckboxDatasetNameData()
137

    
138
  dataSourceRoute = opendataRoute
139
  positionsSourceRoute = positionsRoute
140
  const allPromises = []
141
  data[currentTime] = {}
142

    
143
  const dataSelectedHandler = async (datasetName) => {
144
    if (!(datasetName in dataSourceMarks)) {
145
      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
146
    }
147

    
148
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
149
    data[currentTime][datasetName] = datasetData
150
  }
151
  datasetSelected.forEach((datasetName) => {
152
    allPromises.push(dataSelectedHandler(datasetName))
153
  })
154

    
155
  loadingY(loaderDelay)
156

    
157
  await Promise.all(allPromises).then(
158
    () => {
159
      loadingN(0)
160
      drawMapMarkers(dataSourceMarks)
161
      drawHeatmap(data[currentTime])
162
      
163
      preload(currentTime, 1, currentDateToString())
164
      preload(currentTime, -1, currentDateToString())
165
    }
166
  )
167
}
168

    
169
/**
170
 * Checks dataset availibility
171
 * @param {string} route authority for datasets availibility checks
172
 */
173
function checkDataSetsAvailability (route) {
174
  $.ajax({
175
    type: 'POST',
176
    // Todo it might be good idea to change db collections format
177
    url: route + '/' + currentDateToString(),
178
    success: function (result) {
179
      updateAvailableDataSets(result)
180
    }
181
  })
182
}
183

    
184

    
185

    
186

    
187
/* ------------ MAP ------------ */
188

    
189
/**
190
 * Initialize leaflet map on start position which can be default or set based on user action
191
 */
192
function initMap () {
193
  startX = localStorage.getItem('lat') || startX
194
  startY = localStorage.getItem('lng') || startY
195
  startZoom = localStorage.getItem('zoom') || startZoom
196

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

    
199
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
200
    attribution: '',
201
    maxZoom: 19
202
  }).addTo(mymap)
203

    
204
  mymap.on('click', function (e) { showInfo(e) })
205
}
206

    
207
function setMapView (latitude, longitude, zoom) {
208
  localStorage.setItem('lat', latitude)
209
  localStorage.setItem('lng', longitude)
210
  localStorage.setItem('zoom', zoom)
211

    
212
  mymap.setView([latitude, longitude], zoom)
213
}
214

    
215
function drawHeatmap (dataRaw) {
216
  // Todo still switched
217
  const dataDict = dataRaw
218
  const mergedPoints = []
219
  let max = 0
220

    
221
  if (Object.keys(globalMarkersChanged).length) {
222
    Object.keys(globalMarkersChanged).forEach(function (key) {
223
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
224
    })
225
    globalMarkersChanged = {}
226
  }
227

    
228
  Object.keys(dataDict).forEach((key) => {
229
    const data = dataDict[key]
230
    max = Math.max(max, data.max)
231

    
232
    if (data != null) {
233
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
234
      const points = data.items.map((point) => {
235
        const { x, y, number } = point
236
        const key = x + '' + y
237
        const holder = globalMarkersHolder[key]
238
        if (!globalMarkersChanged[key] && number) {
239
          // There is data for this marker => unbind popup with zero value
240
          holder[0] = holder[0].unbindPopup()
241
          globalMarkersChanged[key] = holder
242
        }
243

    
244
        return [x, y, number]
245
      })
246
      mergedPoints.push(...points)
247
    } else {
248
      if (heatmapLayer != null) {
249
        mymap.removeLayer(heatmapLayer)
250
      }
251
    }
252
  })
253

    
254
  if (heatmapLayer != null) {
255
    mymap.removeLayer(heatmapLayer)
256
  }
257

    
258
  if (mergedPoints.length) {
259
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
260
  }
261

    
262
  // timto vyresen bug #8191 - TODO: znamena to, ze muzeme smazat volani updatePopup() ve funkcich, kde se nejdriv vola drawHeatmap() a pak updatePopup()?
263
  updatePopup()
264
}
265

    
266
function drawMapMarkers (data) {
267
  if (markersLayer != null) {
268
    mymap.removeLayer(markersLayer)
269
  }
270

    
271
  markersLayer = L.layerGroup()
272

    
273
  Object.keys(data).forEach(key_ => {
274
    for (var key in data[key_]) {
275
      const { x, y, name } = data[key_][key]
276
      const pop =
277
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
278
            .setContent(getPopupContent(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
279
      const newCircle =
280
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
281
          .bindPopup(pop)
282
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
283
      markersLayer.addLayer(
284
        newCircle
285
      )
286
    }
287
  })
288

    
289
  markersLayer.setZIndex(-1).addTo(mymap)
290
}
291

    
292

    
293

    
294

    
295
/* ------------ GUI ------------ */
296

    
297
const changeCurrentTime = (time = null) => {
298
  if (time !== null) {
299
    currentTime = time
300
  } else {
301
    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
302
  }
303
}
304

    
305
const changeCurrentDate = (date = null) => {
306
  const dateInput = $('#date')
307
  currentDate = new Date(date ? date : dateInput.val())
308

    
309
  dateInput.val(currentDateToString())
310
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
311

    
312
  data = []
313
}
314

    
315
const toggleDayLock = () => {
316
  lockedDay = !lockedDay
317
  $('#player-date').toggleClass('lock')
318
}
319

    
320

    
321

    
322

    
323
/* ------------ POPUPs ------------ */
324

    
325
const setGlobalPopupContent = (content) => {
326
  globalPopup._popup.setContent(content)
327
  globalPopup._popup.openOn(mymap)
328
}
329

    
330
const getPaginationButtonsInPopup = (currentPage, countPages) => ({
331
  previousButton: '<button id="btn-previous-page" class="circle-button" onclick="setPreviousPageInPopup()"></button>',
332
  pagesList: `<div id="pages">${currentPage} z ${countPages}</div>`,
333
  nextButton: '<button id="btn-next-page" class="circle-button next" onclick="setNextPageInPopup()"></button>'
334
})
335

    
336
const disablePopupPaginationButtons = () => {
337
  $('#btn-previous-page').prop('disabled', true)
338
  $('#btn-next-page').prop('disabled', true)
339
  $('.popup-controls').hide()
340
}
341

    
342
const generatePopupPaginationButtons = (controls) => {
343
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
344
}
345

    
346
const getCountPagesInPopup = () => {
347
  const infoKeys = Object.keys(info)
348

    
349
  if (infoKeys.length === 1) {
350
    // return number of records in one dataset (one dataset in area)
351
    return info[infoKeys[0]].items.length
352
  }
353
  // return number of datasets (agregation of all datasets in area)
354
  return infoKeys.length
355
}
356

    
357
const getPopupDataOnPage = (pageInPopup) => {
358
  const keys = Object.keys(info)
359
  return info[keys[pageInPopup]]
360
}
361

    
362
function setPreviousPageInPopup () {
363
  const countPagesInPopup = getCountPagesInPopup()
364
  const page = currentPageInPopup
365

    
366
  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
367
  setPageContentInPopup(page)
368
}
369

    
370
function setNextPageInPopup () {
371
  const countPagesInPopup = getCountPagesInPopup()
372
  const page = currentPageInPopup
373

    
374
  currentPageInPopup = (currentPageInPopup + 1) % countPagesInPopup
375
  setPageContentInPopup(page)
376
}
377

    
378
const genMultipleDatasetsPopUp = (sum, currentPage, countPages, datasetName) => {
379
  const popupHeader = `<strong id="dataset-name">${datasetName}</strong>`
380
  const popupData = `<div id="place-intesity"><span id="current-number">${sum}</span></div>`
381
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
382

    
383
  return `
384
  ${popupHeader}
385
  ${popupData}
386
  ${generatePopupPaginationButtons([previousButton, pagesList, nextButton])}
387
  `
388
}
389

    
390
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
391
  autoPan: false,
392
  className: className
393
}).setLatLng([lat / num, lng / num])
394

    
395
const getPopupContent = (datasetName, placeName, currentCount, sum, currentPage, countPages) => {
396
  const popupHeader = `
397
    <strong>${datasetName}</strong>
398
    <div id="place-name">${placeName}</div>`
399
  const popupData = `
400
    <div id="place-intesity">
401
      <span id="current-number">${currentCount}</span>
402
      <span id="part-info">${(sum && sum !== Number(currentCount)) ? '/' + sum : ''}</span>
403
    </div>`
404
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
405

    
406
  return `
407
  ${popupHeader}
408
  ${popupData}
409
  ${generatePopupPaginationButtons(countPages > 1 ? [previousButton, pagesList, nextButton] : null)}
410
  `
411
}
412

    
413
const areMultipleDatasetsInRadius = () => {
414
  return Object.keys(info).length > 1
415
}
416

    
417
const setPopupDatasetClassName = (datasetName) => {
418
  const popup = $('.leaflet-popup')
419

    
420
  popup.removeClass(function (index, css) {
421
    return (css.match(/(^|\s)popup-\S+/g) || []).join(' ');
422
  })
423
  popup.addClass('popup-' + datasetName)
424
}
425

    
426
function showInfo (e) {
427
  info = []
428
  currentPageInPopup = 0
429

    
430
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
431
  // Todo change to variable - it is used in heatmap init
432
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
433
  const radius = 25 * stile / 256
434

    
435
  let i = 0
436
  let lat = 0
437
  let lng = 0
438

    
439
  let total = 0
440

    
441
  const datasetsInRadius = {}
442
  const eventCoord = {
443
    lng: e.latlng.lng,
444
    lat: e.latlng.lat
445
  }
446

    
447
  Object.keys(data[currentTime]).forEach(key => {
448
    const namedData = data[currentTime][key]
449

    
450
    namedData.items.forEach(element => {
451
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
452
        lat += element.x
453
        lng += element.y
454
        info[i] = { place: element.place, number: element.number, datasetName: key }
455
        total += parseInt(element.number)
456
        i++
457
        datasetsInRadius[key] = true
458
      }
459
    })
460
  })
461

    
462
  // Process info for more then one dataset
463
  info = info.reduce((acc, item) => {
464
    if (!acc[item.datasetName]) {
465
      acc[item.datasetName] = {
466
        items: [],
467
        number: 0,
468
        datasetName: item.datasetName
469
      }
470
    }
471

    
472
    acc[item.datasetName].items.push(item)
473
    acc[item.datasetName].number += Number(item.number)
474
    return acc
475
  }, {})
476

    
477
  const countDatasets = Object.keys(datasetsInRadius).length
478

    
479
  if (!countDatasets) {
480
    if (mymap._popup) {
481
      $('#part-info').text('')
482
      $('#current-number').html(0)
483
      disablePopupPaginationButtons()
484
    }
485

    
486
    return
487
  }
488

    
489
  if (countDatasets === 1) {
490
    const markersInRadius = getPopupDataOnPage(0)
491
    const popupPagesData = markersInRadius.items
492
    const { place, number } = popupPagesData[currentPageInPopup]
493

    
494
    if (!globalPopup._popup || !areCoordsIdentical(globalPopup.coord, eventCoord)) {
495
      globalPopup._popup = prepareLayerPopUp(lat, lng, i, `popup-${markersInRadius.datasetName}`)
496
      globalPopup.coord = eventCoord
497
    }
498
    else {
499
      setPopupDatasetClassName(markersInRadius.datasetName)
500
    }
501

    
502
    setGlobalPopupContent(getPopupContent(datasetDictNameDisplayName[markersInRadius.datasetName], place, number, total, 1, popupPagesData.length))
503

    
504
    if (popupPagesData.length === 1) {
505
      disablePopupPaginationButtons()
506
    }
507
  } else {
508
    const { datasetName, number } = getPopupDataOnPage(currentPageInPopup)
509

    
510
    if (!globalPopup._popup || !areCoordsIdentical(globalPopup.coord, eventCoord)) {
511
      globalPopup._popup = prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
512
      globalPopup.coord = eventCoord
513
    }
514
    else {
515
      setPopupDatasetClassName(datasetName)
516
    }
517

    
518
    setGlobalPopupContent(genMultipleDatasetsPopUp(number, 1, getCountPagesInPopup(), datasetDictNameDisplayName[datasetName]))
519
  }
520
}
521

    
522
function setPageContentInPopup (page) {
523
  const previousPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(page) : getPopupDataOnPage(0).items[page]
524
  const currentPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(currentPageInPopup) : getPopupDataOnPage(0).items[currentPageInPopup]
525
  const datasetName = $('#dataset-name')
526

    
527
  if (datasetName) {
528
    datasetName.html(datasetDictNameDisplayName[currentPageData.datasetName])
529
  }
530

    
531
  $('#place-name').html(currentPageData.place ? currentPageData.place : currentPageData.datasetName)
532
  $('#current-number').html(currentPageData.number)
533
  $('#pages').html(currentPageInPopup + 1 + ' z ' + getCountPagesInPopup())
534

    
535
  $('.leaflet-popup').removeClass(`popup-${previousPageData.datasetName}`).addClass(`popup-${currentPageData.datasetName}`)
536
}
537

    
538
const updatePopup = () => {
539
  const { _popup } = mymap
540

    
541
  if (_popup) {
542
    showInfo({
543
      latlng: _popup.getLatLng()
544
    })
545
  }
546
}
547

    
548

    
549

    
550

    
551
/* ------------ ANIMATION ------------ */
552

    
553
/**
554
 * Change animation start from playing to stopped or the other way round
555
 */
556
function changeAnimationState () {
557
  const btnAnimate = $('#animate-btn')
558

    
559
  isAnimationRunning = !isAnimationRunning
560

    
561
  if (isAnimationRunning) {
562
    btnAnimate.removeClass('play').addClass('pause')
563
    timer = setInterval(function () { next() }, 800)
564
  } else {
565
    clearTimeout(timer)
566
    btnAnimate.removeClass('pause').addClass('play')
567
  }
568
}
569

    
570
async function previous () {
571
  if (loading) {
572
    return
573
  }
574

    
575
  currentTime = (currentTime + 23) % 24
576
  changeHour(currentTime)
577
  
578
  if (!lockedDay && currentTime === 23) {
579
    addDayToCurrentDate(-1)
580
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
581
  } else {
582
    drawHeatmap(data[currentTime])
583
  }
584

    
585
  updatePopup()
586
}
587

    
588
async function next () {
589
  if (loading) {
590
    return
591
  }
592

    
593
  currentTime = (currentTime + 1) % 24
594
  changeHour(currentTime)
595

    
596
  if (!lockedDay && currentTime === 0) {
597
    addDayToCurrentDate(1)
598
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
599
  } else {
600
    drawHeatmap(data[currentTime])
601
  }
602

    
603
  updatePopup()
604
}
605

    
606
const onChangeHour = (hour) => {
607
  changeHour(hour)
608
  drawHeatmap(data[currentTime])
609
}
610

    
611
const changeHour = (hour) => {
612
  $('#player-time').removeAttr('style')
613
  changeCurrentTime(hour)
614
  updateHeaderControls()
615
  setTimeline()
616
  changeUrlParameters()
617
  updatePopup()
618
}
619

    
620
const dragTimeline = () => {
621
  const hourElemWidthPx = 26
622

    
623
  const elem = $('#player-time')
624
  const offset = elem.offset().left - elem.parent().offset().left
625

    
626
  if (offset >= 0 && offset <= elem.parent().width()) {
627
    const hour = Math.round(offset / hourElemWidthPx)
628

    
629
    if (hour !== currentTime) {
630
      elem.attr('class', 'time hour-' + hour)
631
      $('#player-time span').html(formatTime(hour))
632

    
633
      onChangeHour(hour)
634
    }
635
  }
636
}
637

    
638
const onArrowLeftRightKeysDownRegister = () => {
639
  $(document).keydown(function (e) {
640
    const { which } = e
641
    
642
    if (which === arrowKeyLEFT) {
643
      previous()
644
      e.preventDefault()
645
    } else if (which === arrowKeyRIGHT) {
646
      next()
647
      e.preventDefault()
648
    }
649
  })
650
}
651

    
652
/**
653
 * Change browser url based on animation step.
654
 */
655
const changeUrlParameters = () => {
656
  window.history.pushState(
657
    '',
658
    document.title,
659
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
660
  )
661
}
662

    
663

    
664

    
665

    
666
/* ------------ UTILS ------------ */
667

    
668
const formatTime = (hours, twoDigitsHours = false) => {
669
  return ((twoDigitsHours && hours < 10) ? '0' : '') + hours + ':00';
670
}
671

    
672
const formatDate = (date) => {
673
  var day = String(date.getDate())
674
  var month = String(date.getMonth() + 1)
675

    
676
  if (day.length === 1) {
677
    day = '0' + day
678
  }
679

    
680
  if (month.length === 1) {
681
    month = '0' + month
682
  }
683

    
684
  // return YYYY-MM-DD
685
  return date.getFullYear() + '-' + month + '-' + day
686
}
687

    
688
const currentDayToString = () => {
689
  const day = currentDate.getDate()
690
  return day > 9 ? `${day}` : `0${day}`
691
}
692

    
693
const currentMonthToString = () => {
694
  const month = currentDate.getMonth() + 1
695
  return month > 9 ? `${month}` : `0${month}`
696
}
697

    
698
const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
699

    
700
const addDayToCurrentDate = (day) => {
701
  currentDate.setDate(currentDate.getDate() + day)
702
  changeCurrentDate(currentDate)
703
}
704

    
705
const areCoordsIdentical = (first, second) => {
706
  return first.lat === second.lat && first.lng === second.lng
707
}
708

    
709
const debounce = (func, delay) => {
710
  let inDebounce
711
  return function () {
712
    const context = this
713
    const args = arguments
714
    clearTimeout(inDebounce)
715
    inDebounce = setTimeout(() => func.apply(context, args), delay)
716
  }
717
}
718

    
719

    
720

    
721

    
722
/* ------------ GUI ------------ */
723

    
724
const updateHeaderControls = () => {
725
  $(`#time_${currentTime}`).prop('checked', true)
726
  $('#dropdownMenuButtonTime').html(formatTime(currentTime, true))
727
}
728

    
729
function initDatepicker (availableDatesSource) {
730
  var availableDates = ''
731

    
732
  $.ajax({
733
    type: 'GET',
734
    url: availableDatesSource,
735
    success: function (result) {
736
      availableDates = String(result).split(',')
737
    }
738
  }).then(function () {
739
    $('#date').datepicker({
740
      format: 'yyyy-mm-dd',
741
      language: 'cs',
742
      beforeShowDay: function (date) {
743
        if (availableDates.indexOf(formatDate(date)) < 0) {
744
          return { enabled: false, tooltip: 'Žádná data' }
745
        } else {
746
          return { enabled: true }
747
        }
748
      },
749
      autoclose: true
750
    })
751
  })
752
}
753

    
754
function initLocationsMenu () {
755
  const elmLocationsList = $('.locations')
756
  const locationsDisplayClass = 'show'
757

    
758
  if ($(window).width() <= 480) {
759
    elmLocationsList.removeClass(locationsDisplayClass)
760
  } else {
761
    elmLocationsList.addClass(locationsDisplayClass)
762
  }
763
}
764

    
765
const setTimeline = () => {
766
  $('#player-time > span').text(formatTime(currentTime))
767
  $('#player-time').attr('class', 'time hour-' + currentTime)
768
}
769

    
770
const onValueChangeRegister = () => {
771
  $('#date').change(function () {
772
    changeCurrentDate($(this).val())
773
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
774
    changeUrlParameters()
775
  })
776

    
777
  $('#dropdown-time input[type="radio"]').each(function () {
778
    $(this).change(function () {
779
      changeHour(parseInt($(this).val()))
780
      drawHeatmap(data[currentTime])
781
    })
782
  })
783

    
784
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
785
    $(this).change(
786
      debounce(() => onCheckboxClicked(this), 1000)
787
    )
788
  })
789
}
790

    
791
const onCheckboxClicked = async (checkbox) => {
792
  if ($(checkbox).prop('checked')) {
793
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
794
  } else {
795
    loadCheckboxDatasetNameData()
796

    
797
    data.forEach((item, index) => {
798
      Object.keys(item).forEach(datasetName => {
799
        if (datasetName === $(checkbox).val()) {
800
          delete data[index][datasetName]
801
        }
802
      })
803

    
804
      drawHeatmap(data[currentTime])
805
    })
806
  }
807

    
808
  updatePopup()
809
  changeUrlParameters()
810
}
811

    
812
const loadCheckboxDatasetNameData = () => {
813
  datasetSelected = []
814

    
815
  $('#dropdown-dataset .dropdown-item').each(function () {
816
    const input = $(this).find('input')
817
    const inputVal = input[0].value
818

    
819
    if (input[0].checked) {
820
      datasetSelected.push(inputVal)
821
    }
822

    
823
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
824
  })
825
}
826

    
827
function updateAvailableDataSets (available) {
828
  let leastOneOptionEnabled = false
829

    
830
  $('#dropdown-dataset .dropdown-item').each(function () {
831
    const input = $(this).find('input')
832

    
833
    if (!(input[0].value in available)) {
834
      $(this).addClass('disabled')
835
      $(input).prop('checked', false)
836
    } else {
837
      leastOneOptionEnabled = true
838
      $(this).removeClass('disabled')
839
    }
840
  })
841

    
842
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
843
}
844

    
845

    
846

    
847

    
848
/* ------------ GUI LOADING ------------ */
849

    
850
const loadingCallbackNested = (func, delay) => {
851
  setTimeout(() => {
852
    func(loading)
853
    if (loading) {
854
      loadingCallbackNested(func, delay)
855
    }
856
  }, delay)
857
}
858

    
859
const loadingY = (delay = defaultLoaderDelay) => {
860
  loading++
861
  // check after nms if there is something that is loading
862
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
863
}
864

    
865
const loadingN = (delay = defaultLoaderDelay) => {
866
  loading--
867
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
868
}
869

    
870
const loadingTimeline = (isLoading) => {
871
  if (isLoading) {
872
    loadingYTimeline()
873
  } else {
874
    loadingNTimeline()
875
  }
876
}
877

    
878
const loadingYTimeline = () => {
879
  $('#player-time > .spinner-border').removeClass('d-none')
880
  $('#player-time > span').text('')
881
}
882

    
883
const loadingNTimeline = () => {
884
  $('#player-time > .spinner-border').addClass('d-none')
885
  setTimeline()
886
}
(2-2/2)