Projekt

Obecné

Profil

« Předchozí | Další » 

Revize c8718599

Přidáno uživatelem Martin Sebela před více než 3 roky(ů)

Re #8163 - refactoring

Zobrazit rozdíly:

website/public/js/zcu-heatmap.js
3 3

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

  
8 8
// values for arrow keys
9 9
const arrowKeyLEFT = 37
......
66 66
  _popup: null
67 67
}
68 68

  
69
const setGlobalPopupContent = (content) => {
70
  globalPopup._popup.setContent(content)
71
  globalPopup._popup.openOn(mymap)
72
}
73

  
74
const disablePopupControls = () => {
75
  $('#btn-previous-page').prop('disabled', true)
76
  $('#btn-next-page').prop('disabled', true)
77
  $('.popup-controls').hide()
78
}
79 69

  
80
const areCoordsIdentical = (first, second) => {
81
  return first.lat === second.lat && first.lng === second.lng
82
}
83 70

  
84
const loadingCallbackNested = (func, delay) => {
85
  setTimeout(() => {
86
    func(loading)
87
    if (loading) {
88
      loadingCallbackNested(func, delay)
89
    }
90
  }, delay)
91
}
92

  
93
const loadingY = (delay = defaultLoaderDelay) => {
94
  loading++
95
  // check after nms if there is something that is loading
96
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
97
}
98

  
99
const loadingN = (delay = defaultLoaderDelay) => {
100
  loading--
101
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
102
}
103

  
104
const changeCurrentTime = (time = null) => {
105
  if (time !== null) {
106
    currentTime = time
107
  } else {
108
    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
109
  }
110
}
111 71

  
112
const changeCurrentDate = (date = null) => {
113
  const dateInput = $('#date')
114
  currentDate = new Date(date ? date : dateInput.val())
115

  
116
  dateInput.val(currentDateToString())
117
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
118

  
119
  data = []
120
}
72
function onDocumentReady () {
73
  $('#dropdown-dataset').on('click', function (e) {
74
    e.stopPropagation()
75
  })
121 76

  
122
const currentDayToString = () => {
123
  const day = currentDate.getDate()
124
  return day > 9 ? `${day}` : `0${day}`
77
  $('#btn-update-heatmap').prop('name', '')
78
  changeCurrentTime()
79
  changeCurrentDate()
80
  onValueChangeRegister()
81
  onArrowLeftRightKeysDownRegister()
125 82
}
126 83

  
127
const currentMonthToString = () => {
128
  const month = currentDate.getMonth() + 1
129
  return month > 9 ? `${month}` : `0${month}`
130
}
131 84

  
132
const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
133 85

  
134
const addDayToCurrentDate = (day) => {
135
  currentDate.setDate(currentDate.getDate() + day)
136
  changeCurrentDate(currentDate)
137
}
138 86

  
139
const toggleDayLock = () => {
140
  lockedDay = !lockedDay
141
  $('#player-date').toggleClass('lock')
142
}
87
/* ------------ DATA FETCHERS ------------ */
143 88

  
144 89
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
145 90
  const headers = new Headers()
......
163 108
  return beforeJson.json()
164 109
}
165 110

  
166
const getPaginationButtonsInPopup = (currentPage, countPages) => ({
167
  previousButton: '<button id="btn-previous-page" class="circle-button" onclick="setPreviousPageInPopup()"></button>',
168
  pagesList: `<div id="pages">${currentPage} z ${countPages}</div>`,
169
  nextButton: '<button id="btn-next-page" class="circle-button next" onclick="setNextPageInPopup()"></button>'
170
})
111
async function preload (time, change, date) {
112
  loadingY()
171 113

  
172
const genPopUpControls = (controls) => {
173
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
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()
174 127
}
175 128

  
176
const genMultipleDatasetsPopUp = (sum, currentPage, countPages, datasetName) => {
177
  const popupHeader = `<strong id="dataset-name">${datasetName}</strong>`
178
  const popupData = `<div id="place-intesity"><span id="current-number">${sum}</span></div>`
179
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
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()
180 137

  
181
  return `
182
  ${popupHeader}
183
  ${popupData}
184
  ${genPopUpControls([previousButton, pagesList, nextButton])}
185
  `
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
  )
186 167
}
187 168

  
188
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
189
  autoPan: false,
190
  className: className
191
}).setLatLng([lat / num, lng / num])
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
}
192 183

  
193
const getPopupContent = (datasetName, placeName, currentCount, sum, currentPage, countPages) => {
194
  const popupHeader = `
195
    <strong>${datasetName}</strong>
196
    <div id="place-name">${placeName}</div>`
197
  const popupData = `
198
    <div id="place-intesity">
199
      <span id="current-number">${currentCount}</span>
200
      <span id="part-info">${(sum && sum !== Number(currentCount)) ? '/' + sum : ''}</span>
201
    </div>`
202
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
203 184

  
204
  return `
205
  ${popupHeader}
206
  ${popupData}
207
  ${genPopUpControls(countPages > 1 ? [previousButton, pagesList, nextButton] : null)}
208
  `
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) })
209 205
}
210 206

  
211
const onCheckboxClicked = async (checkbox) => {
212
  if ($(checkbox).prop('checked')) {
213
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
214
  } else {
215
    loadCheckboxDatasetNameData()
207
function setMapView (latitude, longitude, zoom) {
208
  localStorage.setItem('lat', latitude)
209
  localStorage.setItem('lng', longitude)
210
  localStorage.setItem('zoom', zoom)
216 211

  
217
    data.forEach((item, index) => {
218
      Object.keys(item).forEach(datasetName => {
219
        if (datasetName === $(checkbox).val()) {
220
          delete data[index][datasetName]
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
221 242
        }
243

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

  
224
      drawHeatmap(data[currentTime])
225
    })
254
  if (heatmapLayer != null) {
255
    mymap.removeLayer(heatmapLayer)
226 256
  }
227 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()?
228 263
  updatePopup()
229
  changeUrlParameters()
230 264
}
231 265

  
232
const onArrowLeftRightKeysDownRegister = () => {
233
  $(document).keydown(function (e) {
234
    const { which } = e
235
    
236
    if (which === arrowKeyLEFT) {
237
      previous()
238
      e.preventDefault()
239
    } else if (which === arrowKeyRIGHT) {
240
      next()
241
      e.preventDefault()
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
      )
242 286
    }
243 287
  })
288

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

  
246
const debounce = (func, delay) => {
247
  let inDebounce
248
  return function () {
249
    const context = this
250
    const args = arguments
251
    clearTimeout(inDebounce)
252
    inDebounce = setTimeout(() => func.apply(context, args), delay)
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())
253 302
  }
254 303
}
255 304

  
256
const onValueChangeRegister = () => {
257
  $('#date').change(function () {
258
    changeCurrentDate($(this).val())
259
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
260
    changeUrlParameters()
261
  })
305
const changeCurrentDate = (date = null) => {
306
  const dateInput = $('#date')
307
  currentDate = new Date(date ? date : dateInput.val())
262 308

  
263
  $('#dropdown-time input[type="radio"]').each(function () {
264
    $(this).change(function () {
265
      changeHour(parseInt($(this).val()))
266
      drawHeatmap(data[currentTime])
267
    })
268
  })
309
  dateInput.val(currentDateToString())
310
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
269 311

  
270
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
271
    $(this).change(
272
      debounce(() => onCheckboxClicked(this), 1000)
273
    )
274
  })
312
  data = []
275 313
}
276 314

  
277
/**
278
 * Initialize leaflet map on start position which can be default or set based on user action
279
 */
280
// eslint-disable-next-line no-unused-vars
281
function initMap () {
282
  startX = localStorage.getItem('lat') || startX
283
  startY = localStorage.getItem('lng') || startY
284
  startZoom = localStorage.getItem('zoom') || startZoom
315
const toggleDayLock = () => {
316
  lockedDay = !lockedDay
317
  $('#player-date').toggleClass('lock')
318
}
285 319

  
286
  mymap = L.map('heatmap').setView([startX, startY], startZoom)
287 320

  
288
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
289
    attribution: '',
290
    maxZoom: 19
291
  }).addTo(mymap)
292 321

  
293
  mymap.on('click', function (e) { showInfo(e) })
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>`
294 344
}
295 345

  
296 346
const getCountPagesInPopup = () => {
......
309 359
  return info[keys[pageInPopup]]
310 360
}
311 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

  
312 413
const areMultipleDatasetsInRadius = () => {
313 414
  return Object.keys(info).length > 1
314 415
}
......
379 480
    if (mymap._popup) {
380 481
      $('#part-info').text('')
381 482
      $('#current-number').html(0)
382
      disablePopupControls()
483
      disablePopupPaginationButtons()
383 484
    }
384 485

  
385 486
    return
......
401 502
    setGlobalPopupContent(getPopupContent(datasetDictNameDisplayName[markersInRadius.datasetName], place, number, total, 1, popupPagesData.length))
402 503

  
403 504
    if (popupPagesData.length === 1) {
404
      disablePopupControls()
505
      disablePopupPaginationButtons()
405 506
    }
406 507
  } else {
407 508
    const { datasetName, number } = getPopupDataOnPage(currentPageInPopup)
......
418 519
  }
419 520
}
420 521

  
421
// eslint-disable-next-line no-unused-vars
422
function setPreviousPageInPopup () {
423
  const countPagesInPopup = getCountPagesInPopup()
424
  const page = currentPageInPopup
425

  
426
  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
427
  setPageContentInPopup(page)
428
}
429

  
430
// eslint-disable-next-line no-unused-vars
431
function setNextPageInPopup () {
432
  const countPagesInPopup = getCountPagesInPopup()
433
  const page = currentPageInPopup
434

  
435
  currentPageInPopup = (currentPageInPopup + 1) % countPagesInPopup
436
  setPageContentInPopup(page)
437
}
438

  
439 522
function setPageContentInPopup (page) {
440 523
  const previousPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(page) : getPopupDataOnPage(0).items[page]
441 524
  const currentPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(currentPageInPopup) : getPopupDataOnPage(0).items[currentPageInPopup]
......
452 535
  $('.leaflet-popup').removeClass(`popup-${previousPageData.datasetName}`).addClass(`popup-${currentPageData.datasetName}`)
453 536
}
454 537

  
455
// eslint-disable-next-line no-unused-vars
456
function setMapView (latitude, longitude, zoom) {
457
  localStorage.setItem('lat', latitude)
458
  localStorage.setItem('lng', longitude)
459
  localStorage.setItem('zoom', zoom)
538
const updatePopup = () => {
539
  const { _popup } = mymap
460 540

  
461
  mymap.setView([latitude, longitude], zoom)
541
  if (_popup) {
542
    showInfo({
543
      latlng: _popup.getLatLng()
544
    })
545
  }
462 546
}
463 547

  
548

  
549

  
550

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

  
464 553
/**
465 554
 * Change animation start from playing to stopped or the other way round
466 555
 */
467
// eslint-disable-next-line no-unused-vars
468 556
function changeAnimationState () {
469 557
  const btnAnimate = $('#animate-btn')
470 558

  
......
479 567
  }
480 568
}
481 569

  
482
// eslint-disable-next-line no-unused-vars
483 570
async function previous () {
484 571
  if (loading) {
485 572
    return
......
516 603
  updatePopup()
517 604
}
518 605

  
519
/**
520
 * Change browser url based on animation step.
521
 */
522
const changeUrlParameters = () => {
523
  window.history.pushState(
524
    '',
525
    document.title,
526
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
527
  )
528
}
529

  
530
const formatTime = (hours, twoDigitsHours = false) => {
531
  return ((twoDigitsHours && hours < 10) ? '0' : '') + hours + ':00';
532
}
533

  
534
const updateHeaderControls = () => {
535
  $(`#time_${currentTime}`).prop('checked', true)
536
  $('#dropdownMenuButtonTime').html(formatTime(currentTime, true))
537
}
538

  
539
const setTimeline = () => {
540
  $('#player-time > span').text(formatTime(currentTime))
541
  $('#player-time').attr('class', 'time hour-' + currentTime)
542
}
543

  
544
const loadingTimeline = (isLoading) => {
545
  if (isLoading) {
546
    loadingYTimeline()
547
  } else {
548
    loadingNTimeline()
549
  }
550
}
551

  
552
const loadingYTimeline = () => {
553
  $('#player-time > .spinner-border').removeClass('d-none')
554
  $('#player-time > span').text('')
555
}
556

  
557
const loadingNTimeline = () => {
558
  $('#player-time > .spinner-border').addClass('d-none')
559
  setTimeline()
560
}
561

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

  
567 611
const changeHour = (hour) => {
568
  $('#player-time').removeAttr('style')
569
  changeCurrentTime(hour)
570
  updateHeaderControls()
571
  setTimeline()
572
  changeUrlParameters()
573
  updatePopup()
574
}
575

  
576
const updatePopup = () => {
577
  const { _popup } = mymap
578

  
579
  if (_popup) {
580
    showInfo({
581
      latlng: _popup.getLatLng()
582
    })
583
  }
584
}
585

  
586
/**
587
 * Load and display heatmap layer for current data
588
 * @param {string} opendataRoute route to dataset source
589
 * @param {string} positionsRoute  route to dataset postitions source
590
 */
591
// eslint-disable-next-line no-unused-vars
592
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
593
  loadCheckboxDatasetNameData()
594

  
595
  dataSourceRoute = opendataRoute
596
  positionsSourceRoute = positionsRoute
597
  const allPromises = []
598
  data[currentTime] = {}
599

  
600
  const dataSelectedHandler = async (datasetName) => {
601
    if (!(datasetName in dataSourceMarks)) {
602
      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
603
    }
604

  
605
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
606
    data[currentTime][datasetName] = datasetData
607
  }
608
  datasetSelected.forEach((datasetName) => {
609
    allPromises.push(dataSelectedHandler(datasetName))
610
  })
611

  
612
  loadingY(loaderDelay)
613

  
614
  await Promise.all(allPromises).then(
615
    () => {
616
      loadingN(0)
617
      drawDataSourceMarks(dataSourceMarks)
618
      drawHeatmap(data[currentTime])
619
      preload(currentTime, 1, currentDateToString())
620
      preload(currentTime, -1, currentDateToString())
621
    }
622
  )
623
}
624

  
625
function drawDataSourceMarks (data) {
626
  if (marksLayer != null) {
627
    mymap.removeLayer(marksLayer)
628
  }
629

  
630
  marksLayer = L.layerGroup()
631

  
632
  Object.keys(data).forEach((key_) => {
633
    for (var key in data[key_]) {
634
      const { x, y, name } = data[key_][key]
635
      const pop =
636
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
637
            .setContent(getPopupContent(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
638
      const newCircle =
639
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
640
          .bindPopup(pop)
641
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
642
      marksLayer.addLayer(
643
        newCircle
644
      )
645
    }
646
  })
647

  
648
  marksLayer.setZIndex(-1).addTo(mymap)
649
}
650

  
651
async function preload (time, change, date) {
652
  loadingY()
653

  
654
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
655
    if (!data[nTime]) {
656
      data[nTime] = {}
657
    }
658

  
659
    datasetSelected.forEach(async (datasetName) => {
660
      if (!data[nTime][datasetName]) {
661
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
662
      }
663
    })
664
  }
665

  
666
  loadingN()
612
  $('#player-time').removeAttr('style')
613
  changeCurrentTime(hour)
614
  updateHeaderControls()
615
  setTimeline()
616
  changeUrlParameters()
617
  updatePopup()
667 618
}
668 619

  
669
function drawHeatmap (dataRaw) {
670
  // Todo still switched
671
  const dataDict = dataRaw
672
  const mergedPoints = []
673
  let max = 0
620
const dragTimeline = () => {
621
  const hourElemWidthPx = 26
674 622

  
675
  if (Object.keys(globalMarkersChanged).length) {
676
    Object.keys(globalMarkersChanged).forEach(function (key) {
677
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
678
    })
679
    globalMarkersChanged = {}
680
  }
623
  const elem = $('#player-time')
624
  const offset = elem.offset().left - elem.parent().offset().left
681 625

  
682
  Object.keys(dataDict).forEach((key) => {
683
    const data = dataDict[key]
684
    max = Math.max(max, data.max)
626
  if (offset >= 0 && offset <= elem.parent().width()) {
627
    const hour = Math.round(offset / hourElemWidthPx)
685 628

  
686
    if (data != null) {
687
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
688
      const points = data.items.map((point) => {
689
        const { x, y, number } = point
690
        const key = x + '' + y
691
        const holder = globalMarkersHolder[key]
692
        if (!globalMarkersChanged[key] && number) {
693
          // There is data for this marker => unbind popup with zero value
694
          holder[0] = holder[0].unbindPopup()
695
          globalMarkersChanged[key] = holder
696
        }
629
    if (hour !== currentTime) {
630
      elem.attr('class', 'time hour-' + hour)
631
      $('#player-time span').html(formatTime(hour))
697 632

  
698
        return [x, y, number]
699
      })
700
      mergedPoints.push(...points)
701
    } else {
702
      if (heatmapLayer != null) {
703
        mymap.removeLayer(heatmapLayer)
704
      }
633
      onChangeHour(hour)
705 634
    }
706
  })
707

  
708
  if (heatmapLayer != null) {
709
    mymap.removeLayer(heatmapLayer)
710
  }
711

  
712
  if (mergedPoints.length) {
713
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
714 635
  }
636
}
715 637

  
716
  // timto vyresen bug #8191 - TODO: znamena to, ze muzeme smazat volani updatePopup() ve funkcich, kde se nejdriv vola drawHeatmap() a pak updatePopup()?
717
  updatePopup()
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
  })
718 650
}
719 651

  
720 652
/**
721
 * Checks dataset availibility
722
 * @param {string} route authority for datasets availibility checks
653
 * Change browser url based on animation step.
723 654
 */
724
// eslint-disable-next-line no-unused-vars
725
function checkDataSetsAvailability (route) {
726
  $.ajax({
727
    type: 'POST',
728
    // Todo it might be good idea to change db collections format
729
    url: route + '/' + currentDateToString(),
730
    success: function (result) {
731
      updateAvailableDataSets(result)
732
    }
733
  })
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
  )
734 661
}
735 662

  
736
function updateAvailableDataSets (available) {
737
  let leastOneOptionEnabled = false
738 663

  
739
  $('#dropdown-dataset .dropdown-item').each(function () {
740
    const input = $(this).find('input')
741
    const inputVal = input[0].value
742 664

  
743
    if (!(inputVal in available)) {
744
      $(this).addClass('disabled')
745
      $(input).prop('checked', false)
746
    } else {
747
      leastOneOptionEnabled = true
748
      $(this).removeClass('disabled')
749
    }
750
  })
751 665

  
752
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
666
/* ------------ UTILS ------------ */
667

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

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

  
......
764 681
    month = '0' + month
765 682
  }
766 683

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

  
770
// eslint-disable-next-line no-unused-vars
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

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

  
......
804 762
  }
805 763
}
806 764

  
807
function onDocumentReady () {
808
  $('#dropdown-dataset').on('click', function (e) {
809
    e.stopPropagation()
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()
810 775
  })
811 776

  
812
  $('#btn-update-heatmap').prop('name', '')
813
  changeCurrentTime()
814
  changeCurrentDate()
815
  onValueChangeRegister()
816
  onArrowLeftRightKeysDownRegister()
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()
817 810
}
818 811

  
819 812
const loadCheckboxDatasetNameData = () => {
......
831 824
  })
832 825
}
833 826

  
834
const dragTimeline = () => {
835
  const hourElemWidthPx = 26
827
function updateAvailableDataSets (available) {
828
  let leastOneOptionEnabled = false
836 829

  
837
  const elem = $('#player-time')
838
  const offset = elem.offset().left - elem.parent().offset().left
830
  $('#dropdown-dataset .dropdown-item').each(function () {
831
    const input = $(this).find('input')
839 832

  
840
  if (offset >= 0 && offset <= elem.parent().width()) {
841
    const hour = Math.round(offset / hourElemWidthPx)
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
  })
842 841

  
843
    if (hour !== currentTime) {
844
      elem.attr('class', 'time hour-' + hour)
845
      $('#player-time span').html(formatTime(hour))
842
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
843
}
846 844

  
847
      onChangeHour(hour)
845

  
846

  
847

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

  
850
const loadingCallbackNested = (func, delay) => {
851
  setTimeout(() => {
852
    func(loading)
853
    if (loading) {
854
      loadingCallbackNested(func, delay)
848 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()
849 875
  }
850 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
}

Také k dispozici: Unified diff