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
let currentDate
21

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

    
26
//
27
// info = {
28
//  DATASETNAME: {
29
//    items: Array,
30
//    number: Number,
31
//    datasetName: String
32
// }
33
// }
34
//
35
var info = []
36
let currentPageInPopup = 0
37

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

    
42
// animate data only in one day
43
let lockedDay = false
44

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

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

    
51
// map markers for all datasets
52
const dataMapMarkers = {}
53

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

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

    
68

    
69

    
70

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

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

    
83

    
84

    
85

    
86
/* ------------ DATA FETCHERS ------------ */
87

    
88
const genericFetch = async (route, method) => {
89
  const headers = new Headers()
90
  const request = new Request(route, {
91
    method: method,
92
    headers: headers
93
  })
94
  const beforeJson = await fetch(request)
95

    
96
  return beforeJson.json()
97
}
98

    
99
const fetchDatasetDataByDatetime = async (route, datasetName, date, time) => {
100
  return await genericFetch(route + '/' + datasetName + '/' + date + '/' + time, 'GET')
101
}
102

    
103
const fetchDatasetMapMarkers = async (route, datasetName) => {
104
  return await genericFetch(route + '/' + datasetName, 'GET')
105
}
106

    
107
preload = async (time, timeShift, date) => {
108
  loadingY()
109

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

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

    
122
  loadingN()
123
}
124

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

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

    
139
  const dataSelectedHandler = async (datasetName) => {
140
    if (!(datasetName in dataMapMarkers)) {
141
      dataMapMarkers[datasetName] = await fetchDatasetMapMarkers(positionsRoute, datasetName)
142
    }
143

    
144
    const datasetData = await fetchDatasetDataByDatetime(dataSourceRoute, datasetName, currentDateToString(), currentTime)
145
    data[currentTime][datasetName] = datasetData
146
  }
147

    
148
  datasetSelected.forEach((datasetName) => {
149
    allPromises.push(dataSelectedHandler(datasetName))
150
  })
151

    
152
  loadingY(loaderDelay)
153

    
154
  await Promise.all(allPromises).then(
155
    () => {
156
      loadingN(0)
157
      drawMapMarkers(dataMapMarkers)
158
      drawHeatmap(data[currentTime])
159

    
160
      preload(currentTime, 1, currentDateToString())
161
      preload(currentTime, -1, currentDateToString())
162
    }
163
  )
164
}
165

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

    
175

    
176

    
177

    
178
/* ------------ MAP ------------ */
179

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

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

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

    
195
  mymap.on('click', function (e) { showPopup(e) })
196
}
197

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

    
203
  mymap.setView([latitude, longitude], zoom)
204
}
205

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

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

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

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

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

    
244
  if (heatmapLayer != null) {
245
    mymap.removeLayer(heatmapLayer)
246
  }
247

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

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

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

    
261
  markersLayer = L.layerGroup()
262

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

    
279
  markersLayer.setZIndex(-1).addTo(mymap)
280
}
281

    
282

    
283

    
284

    
285
/* ------------ GUI ------------ */
286

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

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

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

    
302
  data = []
303
}
304

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

    
310

    
311

    
312

    
313
/* ------------ POPUPS ------------ */
314

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

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

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

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

    
336
const getCountPagesInPopup = () => {
337
  const infoKeys = Object.keys(info)
338

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

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

    
352
const setPreviousPageInPopup = () => {
353
  const countPagesInPopup = getCountPagesInPopup()
354
  const page = currentPageInPopup
355

    
356
  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
357
  setPageContentInPopup(page)
358
}
359

    
360
const setNextPageInPopup = () => {
361
  const countPagesInPopup = getCountPagesInPopup()
362
  const page = currentPageInPopup
363

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

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

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

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

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

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

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

    
407
const setPopupDatasetClassName = (datasetName) => {
408
  const popup = $('.leaflet-popup')
409

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

    
416
const showPopup = (e) => {
417
  info = []
418
  currentPageInPopup = 0
419

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

    
425
  let i = 0
426
  let lat = 0
427
  let lng = 0
428

    
429
  let total = 0
430

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

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

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

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

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

    
467
  const countDatasets = Object.keys(datasetsInRadius).length
468

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

    
476
    return
477
  }
478

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

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

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

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

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

    
508
    setGlobalPopupContent(genMultipleDatasetsPopUp(number, 1, getCountPagesInPopup(), datasetDictNameDisplayName[datasetName]))
509
  }
510
}
511

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

    
517
  if (datasetName) {
518
    datasetName.html(datasetDictNameDisplayName[currentPageData.datasetName])
519
  }
520

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

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

    
528
const updatePopup = () => {
529
  const { _popup } = mymap
530

    
531
  if (_popup) {
532
    showPopup({
533
      latlng: _popup.getLatLng()
534
    })
535
  }
536
}
537

    
538

    
539

    
540

    
541
/* ------------ ANIMATION ------------ */
542

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

    
549
  isAnimationRunning = !isAnimationRunning
550

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

    
560
const previous = async () => {
561
  if (loading) {
562
    return
563
  }
564

    
565
  currentTime = (currentTime + 23) % 24
566
  changeHour(currentTime)
567

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

    
575
  updatePopup()
576
}
577

    
578
const next = async () => {
579
  if (loading) {
580
    return
581
  }
582

    
583
  currentTime = (currentTime + 1) % 24
584
  changeHour(currentTime)
585

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

    
593
  updatePopup()
594
}
595

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

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

    
610
const dragTimeline = () => {
611
  const hourElemWidthPx = 26
612

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

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

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

    
623
      onChangeHour(hour)
624
    }
625
  }
626
}
627

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

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

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

    
653

    
654

    
655

    
656
/* ------------ UTILS ------------ */
657

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

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

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

    
670
  if (month.length === 1) {
671
    month = '0' + month
672
  }
673

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

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

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

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

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

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

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

    
709

    
710

    
711

    
712
/* ------------ GUI ------------ */
713

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

    
719
const initDatepicker = async (availableDatesSource) => {
720
  const result = await genericFetch(availableDatesSource, 'GET')
721
  const datesContainData = String(result).split(',')
722

    
723
  $('#date').datepicker({
724
    format: 'yyyy-mm-dd',
725
    language: 'cs',
726
    beforeShowDay: function (date) {
727
      if (datesContainData.indexOf(formatDate(date)) < 0) {
728
        return { enabled: false, tooltip: 'Žádná data' }
729
      } else {
730
        return { enabled: true }
731
      }
732
    },
733
    autoclose: true
734
  })
735
}
736

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

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

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

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

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

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

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

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

    
787
      drawHeatmap(data[currentTime])
788
    })
789
  }
790

    
791
  updatePopup()
792
  changeUrlParameters()
793
}
794

    
795
const loadCheckboxDatasetNameData = () => {
796
  datasetSelected = []
797

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

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

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

    
810
const updateAvailableDataSets = async (available) => {
811
  let leastOneOptionEnabled = false
812

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

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

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

    
828

    
829

    
830

    
831
/* ------------ GUI LOADING ------------ */
832

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

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

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

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

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

    
866
const loadingNTimeline = () => {
867
  $('#player-time > .spinner-border').addClass('d-none')
868
  setTimeline()
869
}
870

    
871
module.exports = {
872
  initDatepicker,
873
  initLocationsMenu,
874
  initMap,
875
  onDocumentReady,
876
  checkDataSetsAvailability,
877
  loadCurrentTimeHeatmap,
878
  dragTimeline,
879
  setPreviousPageInPopup,
880
  setNextPageInPopup,
881
  previous,
882
  next,
883
  toggleDayLock,
884
  changeAnimationState,
885
  onChangeHour,
886
  setMapView
887
}
(2-2/4)