Projekt

Obecné

Profil

Stáhnout (22.5 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
const loadCurrentTimeHeatmap = async (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
const 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
const 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
const drawHeatmap = (dataRaw) => {
216
  const dataDict = dataRaw
217
  const mergedPoints = []
218
  let max = 0
219

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

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

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

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

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

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

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

    
265
const drawMapMarkers = (data) => {
266
  if (markersLayer != null) {
267
    mymap.removeLayer(markersLayer)
268
  }
269

    
270
  markersLayer = L.layerGroup()
271

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

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

    
291

    
292

    
293

    
294
/* ------------ GUI ------------ */
295

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

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

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

    
311
  data = []
312
}
313

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

    
319

    
320

    
321

    
322
/* ------------ POPUPs ------------ */
323

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

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

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

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

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

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

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

    
361
const setPreviousPageInPopup = () => {
362
  const countPagesInPopup = getCountPagesInPopup()
363
  const page = currentPageInPopup
364

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

    
369
const setNextPageInPopup = () => {
370
  const countPagesInPopup = getCountPagesInPopup()
371
  const page = currentPageInPopup
372

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

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

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

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

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

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

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

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

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

    
425
const showInfo = (e) => {
426
  info = []
427
  currentPageInPopup = 0
428

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

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

    
438
  let total = 0
439

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

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

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

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

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

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

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

    
485
    return
486
  }
487

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

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

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

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

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

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

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

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

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

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

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

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

    
547

    
548

    
549

    
550
/* ------------ ANIMATION ------------ */
551

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

    
558
  isAnimationRunning = !isAnimationRunning
559

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

    
569
const previous = async () => {
570
  if (loading) {
571
    return
572
  }
573

    
574
  currentTime = (currentTime + 23) % 24
575
  changeHour(currentTime)
576

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

    
584
  updatePopup()
585
}
586

    
587
const next = async () => {
588
  if (loading) {
589
    return
590
  }
591

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

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

    
602
  updatePopup()
603
}
604

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

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

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

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

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

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

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

    
637
const onArrowLeftRightKeysDownRegister = () => {
638
  $(document).keydown(function (e) {
639
    const { which } = e
640

    
641
    if (which === arrowKeyLEFT) {
642
      previous()
643
      e.preventDefault()
644
    } else if (which === arrowKeyRIGHT) {
645
      next()
646
      e.preventDefault()
647
    }
648
  })
649
}
650

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

    
662

    
663

    
664

    
665
/* ------------ UTILS ------------ */
666

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

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

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

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

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

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

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

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

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

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

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

    
718

    
719

    
720

    
721
/* ------------ GUI ------------ */
722

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

    
728
const initDatepicker = (availableDatesSource) => {
729
  var availableDates = ''
730

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

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

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

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

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

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

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

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

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

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

    
807
  updatePopup()
808
  changeUrlParameters()
809
}
810

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

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

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

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

    
826
const updateAvailableDataSets = async (available) => {
827
  let leastOneOptionEnabled = false
828

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

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

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

    
844

    
845

    
846

    
847
/* ------------ GUI LOADING ------------ */
848

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

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

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

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

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

    
882
const loadingNTimeline = () => {
883
  $('#player-time > .spinner-border').addClass('d-none')
884
  setTimeline()
885
}
886
module.exports = {
887
  initDatepicker,
888
  initLocationsMenu,
889
  initMap,
890
  onDocumentReady,
891
  checkDataSetsAvailability,
892
  loadCurrentTimeHeatmap,
893
  dragTimeline,
894
  setPreviousPageInPopup,
895
  setNextPageInPopup,
896
  previous,
897
  next,
898
  toggleDayLock,
899
  changeAnimationState,
900
  onChangeHour,
901
  setMapView
902
}
(3-3/6)