Projekt

Obecné

Profil

Stáhnout (22.3 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
const 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
const genericFetch = async (route, method) => {
89
  const headers = new Headers()
90
  const myRequest = new Request(route, {
91
    method: method,
92
    headers: headers
93
  })
94
  const beforeJson = await fetch(myRequest)
95

    
96
  return beforeJson.json()
97
}
98
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
99
  return await genericFetch(baseRoute + '/' + name + '/' + date + '/' + currentTime, 'GET')
100
}
101

    
102
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
103
    return await genericFetch(positionRoute + '/' + datasetName, 'GET')
104
}
105

    
106
preload = async (time, change, date) => {
107
  loadingY()
108

    
109
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
110
    if (!data[nTime]) {
111
      data[nTime] = {}
112
    }
113

    
114
    datasetSelected.forEach(async (datasetName) => {
115
      if (!data[nTime][datasetName]) {
116
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
117
      }
118
    })
119
  }
120

    
121
  loadingN()
122
}
123

    
124
/**
125
 * Load and display heatmap layer for current data
126
 * 
127
 * @param {string} opendataRoute route to dataset source
128
 * @param {string} positionsRoute  route to dataset postitions source
129
 */
130
const loadCurrentTimeHeatmap = async (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) => {
131
  loadCheckboxDatasetNameData()
132

    
133
  dataSourceRoute = opendataRoute
134
  positionsSourceRoute = positionsRoute
135
  const allPromises = []
136
  data[currentTime] = {}
137

    
138
  const dataSelectedHandler = async (datasetName) => {
139
    if (!(datasetName in dataSourceMarks)) {
140
      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
141
    }
142

    
143
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
144
    data[currentTime][datasetName] = datasetData
145
  }
146
  datasetSelected.forEach((datasetName) => {
147
    allPromises.push(dataSelectedHandler(datasetName))
148
  })
149

    
150
  loadingY(loaderDelay)
151

    
152
  await Promise.all(allPromises).then(
153
    () => {
154
      loadingN(0)
155
      drawMapMarkers(dataSourceMarks)
156
      drawHeatmap(data[currentTime])
157

    
158
      preload(currentTime, 1, currentDateToString())
159
      preload(currentTime, -1, currentDateToString())
160
    }
161
  )
162
}
163

    
164
/**
165
 * Checks dataset availibility
166
 * @param {string} route authority for datasets availibility checks
167
 */
168
const checkDataSetsAvailability = async (route) => {
169
  const result = await genericFetch(route + '/' + currentDateToString(), 'POST')
170
  updateAvailableDataSets(result)
171
}
172

    
173

    
174

    
175

    
176
/* ------------ MAP ------------ */
177

    
178
/**
179
 * Initialize leaflet map on start position which can be default or set based on user action
180
 */
181
const initMap = () => {
182
  startX = localStorage.getItem('lat') || startX
183
  startY = localStorage.getItem('lng') || startY
184
  startZoom = localStorage.getItem('zoom') || startZoom
185

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

    
188
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
189
    attribution: '',
190
    maxZoom: 19
191
  }).addTo(mymap)
192

    
193
  mymap.on('click', function (e) { showInfo(e) })
194
}
195

    
196
const setMapView = (latitude, longitude, zoom) => {
197
  localStorage.setItem('lat', latitude)
198
  localStorage.setItem('lng', longitude)
199
  localStorage.setItem('zoom', zoom)
200

    
201
  mymap.setView([latitude, longitude], zoom)
202
}
203

    
204
const drawHeatmap = (dataRaw) => {
205
  const dataDict = dataRaw
206
  const mergedPoints = []
207
  let max = 0
208

    
209
  if (Object.keys(globalMarkersChanged).length) {
210
    Object.keys(globalMarkersChanged).forEach(function (key) {
211
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
212
    })
213
    globalMarkersChanged = {}
214
  }
215

    
216
  Object.keys(dataDict).forEach((key) => {
217
    const data = dataDict[key]
218
    max = Math.max(max, data.max)
219

    
220
    if (data != null) {
221
      // Bind back popups for markers (we dont know if there is any data for this marker or not)
222
      const points = data.items.map((point) => {
223
        const { x, y, number } = point
224
        const key = x + '' + y
225
        const holder = globalMarkersHolder[key]
226
        if (!globalMarkersChanged[key] && number) {
227
          // There is data for this marker => unbind popup with zero value
228
          holder[0] = holder[0].unbindPopup()
229
          globalMarkersChanged[key] = holder
230
        }
231

    
232
        return [x, y, number]
233
      })
234
      mergedPoints.push(...points)
235
    } else {
236
      if (heatmapLayer != null) {
237
        mymap.removeLayer(heatmapLayer)
238
      }
239
    }
240
  })
241

    
242
  if (heatmapLayer != null) {
243
    mymap.removeLayer(heatmapLayer)
244
  }
245

    
246
  if (mergedPoints.length) {
247
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
248
  }
249

    
250
  // timto vyresen bug #8191 - TODO: znamena to, ze muzeme smazat volani updatePopup() ve funkcich, kde se nejdriv vola drawHeatmap() a pak updatePopup()?
251
  updatePopup()
252
}
253

    
254
const drawMapMarkers = (data) => {
255
  if (markersLayer != null) {
256
    mymap.removeLayer(markersLayer)
257
  }
258

    
259
  markersLayer = L.layerGroup()
260

    
261
  Object.keys(data).forEach(key_ => {
262
    for (var key in data[key_]) {
263
      const { x, y, name } = data[key_][key]
264
      const pop =
265
        prepareLayerPopUp(x, y, 1, `popup-${key_}`)
266
          .setContent(getPopupContent(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
267
      const newCircle =
268
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
269
          .bindPopup(pop)
270
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
271
      markersLayer.addLayer(
272
        newCircle
273
      )
274
    }
275
  })
276

    
277
  markersLayer.setZIndex(-1).addTo(mymap)
278
}
279

    
280

    
281

    
282

    
283
/* ------------ GUI ------------ */
284

    
285
const changeCurrentTime = (time = null) => {
286
  if (time !== null) {
287
    currentTime = time
288
  } else {
289
    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
290
  }
291
}
292

    
293
const changeCurrentDate = (date = null) => {
294
  const dateInput = $('#date')
295
  currentDate = new Date(date ? date : dateInput.val())
296

    
297
  dateInput.val(currentDateToString())
298
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
299

    
300
  data = []
301
}
302

    
303
const toggleDayLock = () => {
304
  lockedDay = !lockedDay
305
  $('#player-date').toggleClass('lock')
306
}
307

    
308

    
309

    
310

    
311
/* ------------ POPUPs ------------ */
312

    
313
const setGlobalPopupContent = (content) => {
314
  globalPopup._popup.setContent(content)
315
  globalPopup._popup.openOn(mymap)
316
}
317

    
318
const getPaginationButtonsInPopup = (currentPage, countPages) => ({
319
  previousButton: '<button type="button" id="btn-popup-previous-page" onclick="js.setPreviousPageInPopup()"></button>',
320
  pagesList: `<p id="pages">${currentPage} z ${countPages}</p>`,
321
  nextButton: '<button type="button" id="btn-popup-next-page" class="next" onclick="js.setNextPageInPopup()"></button>'
322
})
323

    
324
const disablePopupPaginationButtons = () => {
325
  $('#btn-popup-previous-page').prop('disabled', true)
326
  $('#btn-popup-next-page').prop('disabled', true)
327
  $('.popup-pagination').hide()
328
}
329

    
330
const generatePopupPaginationButtons = (controls) => {
331
  return `<div class="popup-pagination">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
332
}
333

    
334
const getCountPagesInPopup = () => {
335
  const infoKeys = Object.keys(info)
336

    
337
  if (infoKeys.length === 1) {
338
    // return number of records in one dataset (one dataset in area)
339
    return info[infoKeys[0]].items.length
340
  }
341
  // return number of datasets (agregation of all datasets in area)
342
  return infoKeys.length
343
}
344

    
345
const getPopupDataOnPage = (pageInPopup) => {
346
  const keys = Object.keys(info)
347
  return info[keys[pageInPopup]]
348
}
349

    
350
const setPreviousPageInPopup = () => {
351
  const countPagesInPopup = getCountPagesInPopup()
352
  const page = currentPageInPopup
353

    
354
  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
355
  setPageContentInPopup(page)
356
}
357

    
358
const setNextPageInPopup = () => {
359
  const countPagesInPopup = getCountPagesInPopup()
360
  const page = currentPageInPopup
361

    
362
  currentPageInPopup = (currentPageInPopup + 1) % countPagesInPopup
363
  setPageContentInPopup(page)
364
}
365

    
366
const genMultipleDatasetsPopUp = (sum, currentPage, countPages, datasetName) => {
367
  const popupHeader = `<strong id="dataset-name">${datasetName}</strong>`
368
  const popupData = `<div id="place-intesity"><span id="current-number">${sum}</span></div>`
369
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
370

    
371
  return `
372
  ${popupHeader}
373
  ${popupData}
374
  ${generatePopupPaginationButtons([previousButton, pagesList, nextButton])}
375
  `
376
}
377

    
378
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
379
  autoPan: false,
380
  className: className
381
}).setLatLng([lat / num, lng / num])
382

    
383
const getPopupContent = (datasetName, placeName, currentCount, sum, currentPage, countPages) => {
384
  const popupHeader = `
385
    <strong>${datasetName}</strong>
386
    <div id="place-name">${placeName}</div>`
387
  const popupData = `
388
    <div id="place-intesity">
389
      <span id="current-number">${currentCount}</span>
390
      <span id="sum-number">${(sum && sum !== Number(currentCount)) ? '/' + sum : ''}</span>
391
    </div>`
392
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
393

    
394
  return `
395
  ${popupHeader}
396
  ${popupData}
397
  ${generatePopupPaginationButtons(countPages > 1 ? [previousButton, pagesList, nextButton] : null)}
398
  `
399
}
400

    
401
const areMultipleDatasetsInRadius = () => {
402
  return Object.keys(info).length > 1
403
}
404

    
405
const setPopupDatasetClassName = (datasetName) => {
406
  const popup = $('.leaflet-popup')
407

    
408
  popup.removeClass(function (index, css) {
409
    return (css.match(/(^|\s)popup-\S+/g) || []).join(' ');
410
  })
411
  popup.addClass('popup-' + datasetName)
412
}
413

    
414
const showInfo = (e) => {
415
  info = []
416
  currentPageInPopup = 0
417

    
418
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
419
  // Todo change to variable - it is used in heatmap init
420
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
421
  const radius = 25 * stile / 256
422

    
423
  let i = 0
424
  let lat = 0
425
  let lng = 0
426

    
427
  let total = 0
428

    
429
  const datasetsInRadius = {}
430
  const eventCoord = {
431
    lng: e.latlng.lng,
432
    lat: e.latlng.lat
433
  }
434

    
435
  Object.keys(data[currentTime]).forEach(key => {
436
    const namedData = data[currentTime][key]
437

    
438
    namedData.items.forEach(element => {
439
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
440
        lat += element.x
441
        lng += element.y
442
        info[i] = { place: element.place, number: element.number, datasetName: key }
443
        total += parseInt(element.number)
444
        i++
445
        datasetsInRadius[key] = true
446
      }
447
    })
448
  })
449

    
450
  // Process info for more then one dataset
451
  info = info.reduce((acc, item) => {
452
    if (!acc[item.datasetName]) {
453
      acc[item.datasetName] = {
454
        items: [],
455
        number: 0,
456
        datasetName: item.datasetName
457
      }
458
    }
459

    
460
    acc[item.datasetName].items.push(item)
461
    acc[item.datasetName].number += Number(item.number)
462
    return acc
463
  }, {})
464

    
465
  const countDatasets = Object.keys(datasetsInRadius).length
466

    
467
  if (!countDatasets) {
468
    if (mymap._popup) {
469
      $('#sum-number').text('')
470
      $('#current-number').html(0)
471
      disablePopupPaginationButtons()
472
    }
473

    
474
    return
475
  }
476

    
477
  if (countDatasets === 1) {
478
    const markersInRadius = getPopupDataOnPage(0)
479
    const popupPagesData = markersInRadius.items
480
    const { place, number } = popupPagesData[currentPageInPopup]
481

    
482
    if (!globalPopup._popup || !areCoordsIdentical(globalPopup.coord, eventCoord)) {
483
      globalPopup._popup = prepareLayerPopUp(lat, lng, i, `popup-${markersInRadius.datasetName}`)
484
      globalPopup.coord = eventCoord
485
    }
486
    else {
487
      setPopupDatasetClassName(markersInRadius.datasetName)
488
    }
489

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

    
492
    if (popupPagesData.length === 1) {
493
      disablePopupPaginationButtons()
494
    }
495
  } else {
496
    const { datasetName, number } = getPopupDataOnPage(currentPageInPopup)
497

    
498
    if (!globalPopup._popup || !areCoordsIdentical(globalPopup.coord, eventCoord)) {
499
      globalPopup._popup = prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
500
      globalPopup.coord = eventCoord
501
    }
502
    else {
503
      setPopupDatasetClassName(datasetName)
504
    }
505

    
506
    setGlobalPopupContent(genMultipleDatasetsPopUp(number, 1, getCountPagesInPopup(), datasetDictNameDisplayName[datasetName]))
507
  }
508
}
509

    
510
const setPageContentInPopup = (page) => {
511
  const previousPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(page) : getPopupDataOnPage(0).items[page]
512
  const currentPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(currentPageInPopup) : getPopupDataOnPage(0).items[currentPageInPopup]
513
  const datasetName = $('#dataset-name')
514

    
515
  if (datasetName) {
516
    datasetName.html(datasetDictNameDisplayName[currentPageData.datasetName])
517
  }
518

    
519
  $('#place-name').html(currentPageData.place ? currentPageData.place : currentPageData.datasetName)
520
  $('#current-number').html(currentPageData.number)
521
  $('#pages').html(currentPageInPopup + 1 + ' z ' + getCountPagesInPopup())
522

    
523
  $('.leaflet-popup').removeClass(`popup-${previousPageData.datasetName}`).addClass(`popup-${currentPageData.datasetName}`)
524
}
525

    
526
const updatePopup = () => {
527
  const { _popup } = mymap
528

    
529
  if (_popup) {
530
    showInfo({
531
      latlng: _popup.getLatLng()
532
    })
533
  }
534
}
535

    
536

    
537

    
538

    
539
/* ------------ ANIMATION ------------ */
540

    
541
/**
542
 * Change animation start from playing to stopped or the other way round
543
 */
544
const changeAnimationState = () => {
545
  const btnAnimate = $('#animate-btn')
546

    
547
  isAnimationRunning = !isAnimationRunning
548

    
549
  if (isAnimationRunning) {
550
    btnAnimate.removeClass('play').addClass('pause')
551
    timer = setInterval(function () { next() }, 800)
552
  } else {
553
    clearTimeout(timer)
554
    btnAnimate.removeClass('pause').addClass('play')
555
  }
556
}
557

    
558
const previous = async () => {
559
  if (loading) {
560
    return
561
  }
562

    
563
  currentTime = (currentTime + 23) % 24
564
  changeHour(currentTime)
565

    
566
  if (!lockedDay && currentTime === 23) {
567
    addDayToCurrentDate(-1)
568
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
569
  } else {
570
    drawHeatmap(data[currentTime])
571
  }
572

    
573
  updatePopup()
574
}
575

    
576
const next = async () => {
577
  if (loading) {
578
    return
579
  }
580

    
581
  currentTime = (currentTime + 1) % 24
582
  changeHour(currentTime)
583

    
584
  if (!lockedDay && currentTime === 0) {
585
    addDayToCurrentDate(1)
586
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
587
  } else {
588
    drawHeatmap(data[currentTime])
589
  }
590

    
591
  updatePopup()
592
}
593

    
594
const onChangeHour = (hour) => {
595
  changeHour(hour)
596
  drawHeatmap(data[currentTime])
597
}
598

    
599
const changeHour = (hour) => {
600
  $('#player-time').removeAttr('style')
601
  changeCurrentTime(hour)
602
  updateHeaderControls()
603
  setTimeline()
604
  changeUrlParameters()
605
  updatePopup()
606
}
607

    
608
const dragTimeline = () => {
609
  const hourElemWidthPx = 26
610

    
611
  const elem = $('#player-time')
612
  const offset = elem.offset().left - elem.parent().offset().left
613

    
614
  if (offset >= 0 && offset <= elem.parent().width()) {
615
    const hour = Math.round(offset / hourElemWidthPx)
616

    
617
    if (hour !== currentTime) {
618
      elem.attr('class', 'time hour-' + hour)
619
      $('#player-time span').html(formatTime(hour))
620

    
621
      onChangeHour(hour)
622
    }
623
  }
624
}
625

    
626
const onArrowLeftRightKeysDownRegister = () => {
627
  $(document).keydown(function (e) {
628
    const { which } = e
629

    
630
    if (which === arrowKeyLEFT) {
631
      previous()
632
      e.preventDefault()
633
    } else if (which === arrowKeyRIGHT) {
634
      next()
635
      e.preventDefault()
636
    }
637
  })
638
}
639

    
640
/**
641
 * Change browser url based on animation step.
642
 */
643
const changeUrlParameters = () => {
644
  window.history.pushState(
645
    '',
646
    document.title,
647
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
648
  )
649
}
650

    
651

    
652

    
653

    
654
/* ------------ UTILS ------------ */
655

    
656
const formatTime = (hours, twoDigitsHours = false) => {
657
  return ((twoDigitsHours && hours < 10) ? '0' : '') + hours + ':00';
658
}
659

    
660
const formatDate = (date) => {
661
  var day = String(date.getDate())
662
  var month = String(date.getMonth() + 1)
663

    
664
  if (day.length === 1) {
665
    day = '0' + day
666
  }
667

    
668
  if (month.length === 1) {
669
    month = '0' + month
670
  }
671

    
672
  // return YYYY-MM-DD
673
  return date.getFullYear() + '-' + month + '-' + day
674
}
675

    
676
const currentDayToString = () => {
677
  const day = currentDate.getDate()
678
  return day > 9 ? `${day}` : `0${day}`
679
}
680

    
681
const currentMonthToString = () => {
682
  const month = currentDate.getMonth() + 1
683
  return month > 9 ? `${month}` : `0${month}`
684
}
685

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

    
688
const addDayToCurrentDate = (day) => {
689
  currentDate.setDate(currentDate.getDate() + day)
690
  changeCurrentDate(currentDate)
691
}
692

    
693
const areCoordsIdentical = (first, second) => {
694
  return first.lat === second.lat && first.lng === second.lng
695
}
696

    
697
const debounce = (func, delay) => {
698
  let inDebounce
699
  return function () {
700
    const context = this
701
    const args = arguments
702
    clearTimeout(inDebounce)
703
    inDebounce = setTimeout(() => func.apply(context, args), delay)
704
  }
705
}
706

    
707

    
708

    
709

    
710
/* ------------ GUI ------------ */
711

    
712
const updateHeaderControls = () => {
713
  $(`#time_${currentTime}`).prop('checked', true)
714
  $('#dropdownMenuButtonTime').html(formatTime(currentTime, true))
715
}
716

    
717
const initDatepicker = async (availableDatesSource) => {
718
  var availableDates = ''
719
  const result = await genericFetch(availableDatesSource, 'GET')
720
  availableDates = String(result).split(',')
721
  $('#date').datepicker({
722
    format: 'yyyy-mm-dd',
723
    language: 'cs',
724
    beforeShowDay: function (date) {
725
      if (availableDates.indexOf(formatDate(date)) < 0) {
726
        return { enabled: false, tooltip: 'Žádná data' }
727
      } else {
728
        return { enabled: true }
729
      }
730
    },
731
    autoclose: true
732
  })
733
}
734

    
735
const initLocationsMenu = () => {
736
  const elmLocationsList = $('.locations')
737
  const locationsDisplayClass = 'show'
738

    
739
  if ($(window).width() <= 480) {
740
    elmLocationsList.removeClass(locationsDisplayClass)
741
  } else {
742
    elmLocationsList.addClass(locationsDisplayClass)
743
  }
744
}
745

    
746
const setTimeline = () => {
747
  $('#player-time > span').text(formatTime(currentTime))
748
  $('#player-time').attr('class', 'time hour-' + currentTime)
749
}
750

    
751
const onValueChangeRegister = () => {
752
  $('#date').change(function () {
753
    changeCurrentDate($(this).val())
754
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
755
    changeUrlParameters()
756
  })
757

    
758
  $('#dropdown-time input[type="radio"]').each(function () {
759
    $(this).change(function () {
760
      changeHour(parseInt($(this).val()))
761
      drawHeatmap(data[currentTime])
762
    })
763
  })
764

    
765
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
766
    $(this).change(
767
      debounce(() => onCheckboxClicked(this), 1000)
768
    )
769
  })
770
}
771

    
772
const onCheckboxClicked = async (checkbox) => {
773
  if ($(checkbox).prop('checked')) {
774
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
775
  } else {
776
    loadCheckboxDatasetNameData()
777

    
778
    data.forEach((item, index) => {
779
      Object.keys(item).forEach(datasetName => {
780
        if (datasetName === $(checkbox).val()) {
781
          delete data[index][datasetName]
782
        }
783
      })
784

    
785
      drawHeatmap(data[currentTime])
786
    })
787
  }
788

    
789
  updatePopup()
790
  changeUrlParameters()
791
}
792

    
793
const loadCheckboxDatasetNameData = () => {
794
  datasetSelected = []
795

    
796
  $('#dropdown-dataset .dropdown-item').each(function () {
797
    const input = $(this).find('input')
798
    const inputVal = input[0].value
799

    
800
    if (input[0].checked) {
801
      datasetSelected.push(inputVal)
802
    }
803

    
804
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
805
  })
806
}
807

    
808
const updateAvailableDataSets = async (available) => {
809
  let leastOneOptionEnabled = false
810

    
811
  $('#dropdown-dataset .dropdown-item').each(function () {
812
    const input = $(this).find('input')
813

    
814
    if (!(input[0].value in available)) {
815
      $(this).addClass('disabled')
816
      $(input).prop('checked', false)
817
    } else {
818
      leastOneOptionEnabled = true
819
      $(this).removeClass('disabled')
820
    }
821
  })
822

    
823
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
824
}
825

    
826

    
827

    
828

    
829
/* ------------ GUI LOADING ------------ */
830

    
831
const loadingCallbackNested = (func, delay) => {
832
  setTimeout(() => {
833
    func(loading)
834
    if (loading) {
835
      loadingCallbackNested(func, delay)
836
    }
837
  }, delay)
838
}
839

    
840
const loadingY = (delay = defaultLoaderDelay) => {
841
  loading++
842
  // check after nms if there is something that is loading
843
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
844
}
845

    
846
const loadingN = (delay = defaultLoaderDelay) => {
847
  loading--
848
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
849
}
850

    
851
const loadingTimeline = (isLoading) => {
852
  if (isLoading) {
853
    loadingYTimeline()
854
  } else {
855
    loadingNTimeline()
856
  }
857
}
858

    
859
const loadingYTimeline = () => {
860
  $('#player-time > .spinner-border').removeClass('d-none')
861
  $('#player-time > span').text('')
862
}
863

    
864
const loadingNTimeline = () => {
865
  $('#player-time > .spinner-border').addClass('d-none')
866
  setTimeline()
867
}
868
module.exports = {
869
  initDatepicker,
870
  initLocationsMenu,
871
  initMap,
872
  onDocumentReady,
873
  checkDataSetsAvailability,
874
  loadCurrentTimeHeatmap,
875
  dragTimeline,
876
  setPreviousPageInPopup,
877
  setNextPageInPopup,
878
  previous,
879
  next,
880
  toggleDayLock,
881
  changeAnimationState,
882
  onChangeHour,
883
  setMapView
884
}
(2-2/4)