Projekt

Obecné

Profil

Stáhnout (22.2 KB) Statistiky
| Větev: | Revize:
1 8feb1753 ballakt
/* global L */
2
/* global $ */
3 2f227a6c ballakt
4 8feb1753 ballakt
var mymap
5
var heatmapLayer = null
6 c8718599 Martin Sebela
var markersLayer = null
7 03c02899 vastja
8 815159f3 Tomáš Ballák
// values for arrow keys
9
const arrowKeyLEFT = 37
10
const arrowKeyRIGHT = 39
11
12 8feb1753 ballakt
var startX = 49.7248
13
var startY = 13.3521
14
var startZoom = 17
15 3fc08f2d vastja
16 8feb1753 ballakt
var dataSourceRoute
17 1cf1413d ballakt
let positionsSourceRoute
18 1774c06d Tomáš Ballák
19
let currentTime
20
21
let currentDate
22 a48642fb vastja
23 8feb1753 ballakt
var timer
24
var isAnimationRunning = false
25
var data = []
26 a48642fb vastja
27 e6097215 ballakt
//
28
// info = {
29
//  DATASETNAME: {
30
//    items: Array,
31
//    number: Number,
32
//    datasetName: String
33
// }
34
// }
35
//
36 70a3df53 vastja
var info = []
37 1a1d8f64 Martin Sebela
let currentPageInPopup = 0
38 2f227a6c ballakt
39 d51166e8 ballakt
// dictionary for names of datasets
40 883a423e Tomáš Ballák
const datasetDictNameDisplayName = {}
41 2f227a6c ballakt
var datasetSelected = []
42 70a3df53 vastja
43 1774c06d Tomáš Ballák
// data only for one day
44
let lockedDay = false
45
46 d51166e8 ballakt
// loading information for async operations
47 1774c06d Tomáš Ballák
let loading = 0
48
49 d51166e8 ballakt
// default loader showup delay
50
const defaultLoaderDelay = 1000
51
52 1774c06d Tomáš Ballák
// marks for all datasets
53
const dataSourceMarks = {}
54
55 8feb1753 ballakt
const globalMarkersHolder = {}
56 d5a88af0 Tomáš Ballák
// 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 084a5972 ballakt
61 e6097215 ballakt
const globalPopup = {
62
  coord: {
63
    lat: 0,
64
    lng: 0
65
  },
66
  _popup: null
67
}
68 1a1d8f64 Martin Sebela
69 e6097215 ballakt
70
71 1774c06d Tomáš Ballák
72 c8718599 Martin Sebela
function onDocumentReady () {
73
  $('#dropdown-dataset').on('click', function (e) {
74
    e.stopPropagation()
75
  })
76 1a1d8f64 Martin Sebela
77 c8718599 Martin Sebela
  $('#btn-update-heatmap').prop('name', '')
78
  changeCurrentTime()
79
  changeCurrentDate()
80
  onValueChangeRegister()
81
  onArrowLeftRightKeysDownRegister()
82 1774c06d Tomáš Ballák
}
83 1a1d8f64 Martin Sebela
84
85
86
87 c8718599 Martin Sebela
/* ------------ DATA FETCHERS ------------ */
88 c892003d Martin Sebela
89 2f227a6c ballakt
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 1a1d8f64 Martin Sebela
97 2f227a6c ballakt
  return beforeJson.json()
98
}
99 c892003d Martin Sebela
100 2f227a6c ballakt
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 1a1d8f64 Martin Sebela
108 2f227a6c ballakt
  return beforeJson.json()
109
}
110 084a5972 ballakt
111 c8718599 Martin Sebela
async function preload (time, change, date) {
112
  loadingY()
113 c892003d Martin Sebela
114 c8718599 Martin Sebela
  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 084a5972 ballakt
}
128 c892003d Martin Sebela
129 c8718599 Martin Sebela
/**
130
 * Load and display heatmap layer for current data
131
 * 
132
 * @param {string} opendataRoute route to dataset source
133
 * @param {string} positionsRoute  route to dataset postitions source
134
 */
135
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
136
  loadCheckboxDatasetNameData()
137 c892003d Martin Sebela
138 c8718599 Martin Sebela
  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 2f227a6c ballakt
}
168 c892003d Martin Sebela
169 c8718599 Martin Sebela
/**
170
 * Checks dataset availibility
171
 * @param {string} route authority for datasets availibility checks
172
 */
173
function checkDataSetsAvailability (route) {
174
  $.ajax({
175
    type: 'POST',
176
    // Todo it might be good idea to change db collections format
177
    url: route + '/' + currentDateToString(),
178
    success: function (result) {
179
      updateAvailableDataSets(result)
180
    }
181
  })
182
}
183 2f227a6c ballakt
184 bb2d43b5 Martin Sebela
185 c8718599 Martin Sebela
186
187
/* ------------ MAP ------------ */
188
189
/**
190
 * Initialize leaflet map on start position which can be default or set based on user action
191
 */
192
function initMap () {
193
  startX = localStorage.getItem('lat') || startX
194
  startY = localStorage.getItem('lng') || startY
195
  startZoom = localStorage.getItem('zoom') || startZoom
196
197
  mymap = L.map('heatmap').setView([startX, startY], startZoom)
198
199
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
200
    attribution: '',
201
    maxZoom: 19
202
  }).addTo(mymap)
203
204
  mymap.on('click', function (e) { showInfo(e) })
205 084a5972 ballakt
}
206 c892003d Martin Sebela
207 c8718599 Martin Sebela
function setMapView (latitude, longitude, zoom) {
208
  localStorage.setItem('lat', latitude)
209
  localStorage.setItem('lng', longitude)
210
  localStorage.setItem('zoom', zoom)
211 c892003d Martin Sebela
212 c8718599 Martin Sebela
  mymap.setView([latitude, longitude], zoom)
213
}
214
215
function drawHeatmap (dataRaw) {
216
  // Todo still switched
217
  const dataDict = dataRaw
218
  const mergedPoints = []
219
  let max = 0
220
221
  if (Object.keys(globalMarkersChanged).length) {
222
    Object.keys(globalMarkersChanged).forEach(function (key) {
223
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
224
    })
225
    globalMarkersChanged = {}
226
  }
227
228
  Object.keys(dataDict).forEach((key) => {
229
    const data = dataDict[key]
230
    max = Math.max(max, data.max)
231
232
    if (data != null) {
233
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
234
      const points = data.items.map((point) => {
235
        const { x, y, number } = point
236
        const key = x + '' + y
237
        const holder = globalMarkersHolder[key]
238
        if (!globalMarkersChanged[key] && number) {
239
          // There is data for this marker => unbind popup with zero value
240
          holder[0] = holder[0].unbindPopup()
241
          globalMarkersChanged[key] = holder
242 1cf1413d ballakt
        }
243 c8718599 Martin Sebela
244
        return [x, y, number]
245 1cf1413d ballakt
      })
246 c8718599 Martin Sebela
      mergedPoints.push(...points)
247
    } else {
248
      if (heatmapLayer != null) {
249
        mymap.removeLayer(heatmapLayer)
250
      }
251
    }
252
  })
253 1a1d8f64 Martin Sebela
254 c8718599 Martin Sebela
  if (heatmapLayer != null) {
255
    mymap.removeLayer(heatmapLayer)
256 1cf1413d ballakt
  }
257 1a1d8f64 Martin Sebela
258 c8718599 Martin Sebela
  if (mergedPoints.length) {
259
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
260
  }
261
262
  // timto vyresen bug #8191 - TODO: znamena to, ze muzeme smazat volani updatePopup() ve funkcich, kde se nejdriv vola drawHeatmap() a pak updatePopup()?
263 e6097215 ballakt
  updatePopup()
264 1cf1413d ballakt
}
265 c892003d Martin Sebela
266 c8718599 Martin Sebela
function drawMapMarkers (data) {
267
  if (markersLayer != null) {
268
    mymap.removeLayer(markersLayer)
269
  }
270
271
  markersLayer = L.layerGroup()
272
273
  Object.keys(data).forEach(key_ => {
274
    for (var key in data[key_]) {
275
      const { x, y, name } = data[key_][key]
276
      const pop =
277
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
278
            .setContent(getPopupContent(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
279
      const newCircle =
280
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
281
          .bindPopup(pop)
282
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
283
      markersLayer.addLayer(
284
        newCircle
285
      )
286 815159f3 Tomáš Ballák
    }
287
  })
288 c8718599 Martin Sebela
289
  markersLayer.setZIndex(-1).addTo(mymap)
290 815159f3 Tomáš Ballák
}
291
292 c8718599 Martin Sebela
293
294
295
/* ------------ GUI ------------ */
296
297
const changeCurrentTime = (time = null) => {
298
  if (time !== null) {
299
    currentTime = time
300
  } else {
301
    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
302 1cf1413d ballakt
  }
303
}
304 bb2d43b5 Martin Sebela
305 c8718599 Martin Sebela
const changeCurrentDate = (date = null) => {
306
  const dateInput = $('#date')
307
  currentDate = new Date(date ? date : dateInput.val())
308 bb2d43b5 Martin Sebela
309 c8718599 Martin Sebela
  dateInput.val(currentDateToString())
310
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
311 bb2d43b5 Martin Sebela
312 c8718599 Martin Sebela
  data = []
313 1cf1413d ballakt
}
314 bb2d43b5 Martin Sebela
315 c8718599 Martin Sebela
const toggleDayLock = () => {
316
  lockedDay = !lockedDay
317
  $('#player-date').toggleClass('lock')
318
}
319 72a438f3 vastja
320 3fc08f2d vastja
321 3ae59f75 vastja
322 c8718599 Martin Sebela
323
/* ------------ POPUPs ------------ */
324
325
const setGlobalPopupContent = (content) => {
326
  globalPopup._popup.setContent(content)
327
  globalPopup._popup.openOn(mymap)
328
}
329
330
const getPaginationButtonsInPopup = (currentPage, countPages) => ({
331
  previousButton: '<button id="btn-previous-page" class="circle-button" onclick="setPreviousPageInPopup()"></button>',
332
  pagesList: `<div id="pages">${currentPage} z ${countPages}</div>`,
333
  nextButton: '<button id="btn-next-page" class="circle-button next" onclick="setNextPageInPopup()"></button>'
334
})
335
336
const disablePopupPaginationButtons = () => {
337
  $('#btn-previous-page').prop('disabled', true)
338
  $('#btn-next-page').prop('disabled', true)
339
  $('.popup-controls').hide()
340
}
341
342
const generatePopupPaginationButtons = (controls) => {
343
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
344 c236b33a msebela
}
345 c892003d Martin Sebela
346 1a1d8f64 Martin Sebela
const getCountPagesInPopup = () => {
347 2f227a6c ballakt
  const infoKeys = Object.keys(info)
348 894e3339 Martin Sebela
349 2f227a6c ballakt
  if (infoKeys.length === 1) {
350
    // return number of records in one dataset (one dataset in area)
351
    return info[infoKeys[0]].items.length
352
  }
353
  // return number of datasets (agregation of all datasets in area)
354
  return infoKeys.length
355
}
356 c892003d Martin Sebela
357 894e3339 Martin Sebela
const getPopupDataOnPage = (pageInPopup) => {
358 2f227a6c ballakt
  const keys = Object.keys(info)
359 894e3339 Martin Sebela
  return info[keys[pageInPopup]]
360 2f227a6c ballakt
}
361 c892003d Martin Sebela
362 c8718599 Martin Sebela
function setPreviousPageInPopup () {
363
  const countPagesInPopup = getCountPagesInPopup()
364
  const page = currentPageInPopup
365
366
  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
367
  setPageContentInPopup(page)
368
}
369
370
function setNextPageInPopup () {
371
  const countPagesInPopup = getCountPagesInPopup()
372
  const page = currentPageInPopup
373
374
  currentPageInPopup = (currentPageInPopup + 1) % countPagesInPopup
375
  setPageContentInPopup(page)
376
}
377
378
const genMultipleDatasetsPopUp = (sum, currentPage, countPages, datasetName) => {
379
  const popupHeader = `<strong id="dataset-name">${datasetName}</strong>`
380
  const popupData = `<div id="place-intesity"><span id="current-number">${sum}</span></div>`
381
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
382
383
  return `
384
  ${popupHeader}
385
  ${popupData}
386
  ${generatePopupPaginationButtons([previousButton, pagesList, nextButton])}
387
  `
388
}
389
390
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
391
  autoPan: false,
392
  className: className
393
}).setLatLng([lat / num, lng / num])
394
395
const getPopupContent = (datasetName, placeName, currentCount, sum, currentPage, countPages) => {
396
  const popupHeader = `
397
    <strong>${datasetName}</strong>
398
    <div id="place-name">${placeName}</div>`
399
  const popupData = `
400
    <div id="place-intesity">
401
      <span id="current-number">${currentCount}</span>
402
      <span id="part-info">${(sum && sum !== Number(currentCount)) ? '/' + sum : ''}</span>
403
    </div>`
404
  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
405
406
  return `
407
  ${popupHeader}
408
  ${popupData}
409
  ${generatePopupPaginationButtons(countPages > 1 ? [previousButton, pagesList, nextButton] : null)}
410
  `
411
}
412
413 894e3339 Martin Sebela
const areMultipleDatasetsInRadius = () => {
414 2f227a6c ballakt
  return Object.keys(info).length > 1
415
}
416 c892003d Martin Sebela
417 894e3339 Martin Sebela
const setPopupDatasetClassName = (datasetName) => {
418 2c2cad8d Martin Sebela
  const popup = $('.leaflet-popup')
419
420
  popup.removeClass(function (index, css) {
421 894e3339 Martin Sebela
    return (css.match(/(^|\s)popup-\S+/g) || []).join(' ');
422 2c2cad8d Martin Sebela
  })
423 894e3339 Martin Sebela
  popup.addClass('popup-' + datasetName)
424 2c2cad8d Martin Sebela
}
425
426 8feb1753 ballakt
function showInfo (e) {
427 3ae59f75 vastja
  info = []
428 1a1d8f64 Martin Sebela
  currentPageInPopup = 0
429 3ae59f75 vastja
430
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
431
  // Todo change to variable - it is used in heatmap init
432 2f227a6c ballakt
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
433
  const radius = 25 * stile / 256
434
435
  let i = 0
436
  let lat = 0
437
  let lng = 0
438
439
  let total = 0
440
441
  const datasetsInRadius = {}
442 e6097215 ballakt
  const eventCoord = {
443
    lng: e.latlng.lng,
444
    lat: e.latlng.lat
445
  }
446 894e3339 Martin Sebela
447
  Object.keys(data[currentTime]).forEach(key => {
448 2f227a6c ballakt
    const namedData = data[currentTime][key]
449 894e3339 Martin Sebela
450 2f227a6c ballakt
    namedData.items.forEach(element => {
451
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
452
        lat += element.x
453
        lng += element.y
454
        info[i] = { place: element.place, number: element.number, datasetName: key }
455
        total += parseInt(element.number)
456
        i++
457
        datasetsInRadius[key] = true
458
      }
459
    })
460 8feb1753 ballakt
  })
461 1a1d8f64 Martin Sebela
462 2f227a6c ballakt
  // Process info for more then one dataset
463
  info = info.reduce((acc, item) => {
464
    if (!acc[item.datasetName]) {
465
      acc[item.datasetName] = {
466
        items: [],
467
        number: 0,
468
        datasetName: item.datasetName
469
      }
470
    }
471 c892003d Martin Sebela
472 2f227a6c ballakt
    acc[item.datasetName].items.push(item)
473
    acc[item.datasetName].number += Number(item.number)
474
    return acc
475
  }, {})
476
477 894e3339 Martin Sebela
  const countDatasets = Object.keys(datasetsInRadius).length
478 1a1d8f64 Martin Sebela
479 894e3339 Martin Sebela
  if (!countDatasets) {
480 e6097215 ballakt
    if (mymap._popup) {
481
      $('#part-info').text('')
482
      $('#current-number').html(0)
483 c8718599 Martin Sebela
      disablePopupPaginationButtons()
484 e6097215 ballakt
    }
485
486 d5a88af0 Tomáš Ballák
    return
487
  }
488 1a1d8f64 Martin Sebela
489 894e3339 Martin Sebela
  if (countDatasets === 1) {
490
    const markersInRadius = getPopupDataOnPage(0)
491
    const popupPagesData = markersInRadius.items
492
    const { place, number } = popupPagesData[currentPageInPopup]
493 e6097215 ballakt
494 894e3339 Martin Sebela
    if (!globalPopup._popup || !areCoordsIdentical(globalPopup.coord, eventCoord)) {
495
      globalPopup._popup = prepareLayerPopUp(lat, lng, i, `popup-${markersInRadius.datasetName}`)
496 e6097215 ballakt
      globalPopup.coord = eventCoord
497
    }
498 2c2cad8d Martin Sebela
    else {
499 894e3339 Martin Sebela
      setPopupDatasetClassName(markersInRadius.datasetName)
500 2c2cad8d Martin Sebela
    }
501 e6097215 ballakt
502 894e3339 Martin Sebela
    setGlobalPopupContent(getPopupContent(datasetDictNameDisplayName[markersInRadius.datasetName], place, number, total, 1, popupPagesData.length))
503 c892003d Martin Sebela
504 894e3339 Martin Sebela
    if (popupPagesData.length === 1) {
505 c8718599 Martin Sebela
      disablePopupPaginationButtons()
506 3ae59f75 vastja
    }
507 1774c06d Tomáš Ballák
  } else {
508 894e3339 Martin Sebela
    const { datasetName, number } = getPopupDataOnPage(currentPageInPopup)
509 c892003d Martin Sebela
510 894e3339 Martin Sebela
    if (!globalPopup._popup || !areCoordsIdentical(globalPopup.coord, eventCoord)) {
511 e6097215 ballakt
      globalPopup._popup = prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
512
      globalPopup.coord = eventCoord
513
    }
514 2c2cad8d Martin Sebela
    else {
515 894e3339 Martin Sebela
      setPopupDatasetClassName(datasetName)
516 2c2cad8d Martin Sebela
    }
517 e6097215 ballakt
518 1a1d8f64 Martin Sebela
    setGlobalPopupContent(genMultipleDatasetsPopUp(number, 1, getCountPagesInPopup(), datasetDictNameDisplayName[datasetName]))
519 3ae59f75 vastja
  }
520
}
521
522 1a1d8f64 Martin Sebela
function setPageContentInPopup (page) {
523 894e3339 Martin Sebela
  const previousPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(page) : getPopupDataOnPage(0).items[page]
524
  const currentPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(currentPageInPopup) : getPopupDataOnPage(0).items[currentPageInPopup]
525 1a1d8f64 Martin Sebela
  const datasetName = $('#dataset-name')
526 c892003d Martin Sebela
527 1a1d8f64 Martin Sebela
  if (datasetName) {
528
    datasetName.html(datasetDictNameDisplayName[currentPageData.datasetName])
529 883a423e Tomáš Ballák
  }
530 1774c06d Tomáš Ballák
531 1a1d8f64 Martin Sebela
  $('#place-name').html(currentPageData.place ? currentPageData.place : currentPageData.datasetName)
532
  $('#current-number').html(currentPageData.number)
533
  $('#pages').html(currentPageInPopup + 1 + ' z ' + getCountPagesInPopup())
534 c892003d Martin Sebela
535 1a1d8f64 Martin Sebela
  $('.leaflet-popup').removeClass(`popup-${previousPageData.datasetName}`).addClass(`popup-${currentPageData.datasetName}`)
536 3ae59f75 vastja
}
537 351696d5 Martin Sebela
538 c8718599 Martin Sebela
const updatePopup = () => {
539
  const { _popup } = mymap
540 1a1d8f64 Martin Sebela
541 c8718599 Martin Sebela
  if (_popup) {
542
    showInfo({
543
      latlng: _popup.getLatLng()
544
    })
545
  }
546 3fc08f2d vastja
}
547
548 c8718599 Martin Sebela
549
550
551
/* ------------ ANIMATION ------------ */
552
553 70a3df53 vastja
/**
554
 * Change animation start from playing to stopped or the other way round
555
 */
556 8feb1753 ballakt
function changeAnimationState () {
557 1a1d8f64 Martin Sebela
  const btnAnimate = $('#animate-btn')
558
559 a48642fb vastja
  isAnimationRunning = !isAnimationRunning
560 c892003d Martin Sebela
561 a48642fb vastja
  if (isAnimationRunning) {
562 1a1d8f64 Martin Sebela
    btnAnimate.removeClass('play').addClass('pause')
563 1774c06d Tomáš Ballák
    timer = setInterval(function () { next() }, 800)
564
  } else {
565 8feb1753 ballakt
    clearTimeout(timer)
566 1a1d8f64 Martin Sebela
    btnAnimate.removeClass('pause').addClass('play')
567 351696d5 Martin Sebela
  }
568
}
569
570 1774c06d Tomáš Ballák
async function previous () {
571
  if (loading) {
572
    return
573
  }
574 ec5e3220 Martin Sebela
575 8feb1753 ballakt
  currentTime = (currentTime + 23) % 24
576 1774c06d Tomáš Ballák
  changeHour(currentTime)
577 1a1d8f64 Martin Sebela
  
578
  if (!lockedDay && currentTime === 23) {
579 1774c06d Tomáš Ballák
    addDayToCurrentDate(-1)
580
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
581
  } else {
582
    drawHeatmap(data[currentTime])
583
  }
584 1a1d8f64 Martin Sebela
585 e6097215 ballakt
  updatePopup()
586 a48642fb vastja
}
587
588 1774c06d Tomáš Ballák
async function next () {
589
  if (loading) {
590
    return
591
  }
592 ec5e3220 Martin Sebela
593 8feb1753 ballakt
  currentTime = (currentTime + 1) % 24
594 1774c06d Tomáš Ballák
  changeHour(currentTime)
595 1a1d8f64 Martin Sebela
596
  if (!lockedDay && currentTime === 0) {
597 1774c06d Tomáš Ballák
    addDayToCurrentDate(1)
598
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
599
  } else {
600
    drawHeatmap(data[currentTime])
601
  }
602 1a1d8f64 Martin Sebela
603 e6097215 ballakt
  updatePopup()
604 8b840eb7 vastja
}
605 c892003d Martin Sebela
606 1774c06d Tomáš Ballák
const onChangeHour = (hour) => {
607
  changeHour(hour)
608
  drawHeatmap(data[currentTime])
609
}
610 863ca316 Martin Sebela
611 1774c06d Tomáš Ballák
const changeHour = (hour) => {
612 c8718599 Martin Sebela
  $('#player-time').removeAttr('style')
613
  changeCurrentTime(hour)
614
  updateHeaderControls()
615
  setTimeline()
616
  changeUrlParameters()
617
  updatePopup()
618 3fc08f2d vastja
}
619
620 c8718599 Martin Sebela
const dragTimeline = () => {
621
  const hourElemWidthPx = 26
622 1774c06d Tomáš Ballák
623 c8718599 Martin Sebela
  const elem = $('#player-time')
624
  const offset = elem.offset().left - elem.parent().offset().left
625 c892003d Martin Sebela
626 c8718599 Martin Sebela
  if (offset >= 0 && offset <= elem.parent().width()) {
627
    const hour = Math.round(offset / hourElemWidthPx)
628 c892003d Martin Sebela
629 c8718599 Martin Sebela
    if (hour !== currentTime) {
630
      elem.attr('class', 'time hour-' + hour)
631
      $('#player-time span').html(formatTime(hour))
632 c892003d Martin Sebela
633 c8718599 Martin Sebela
      onChangeHour(hour)
634 a48642fb vastja
    }
635
  }
636 c8718599 Martin Sebela
}
637 ac154afa Martin Sebela
638 c8718599 Martin Sebela
const onArrowLeftRightKeysDownRegister = () => {
639
  $(document).keydown(function (e) {
640
    const { which } = e
641
    
642
    if (which === arrowKeyLEFT) {
643
      previous()
644
      e.preventDefault()
645
    } else if (which === arrowKeyRIGHT) {
646
      next()
647
      e.preventDefault()
648
    }
649
  })
650 03c02899 vastja
}
651 3fc08f2d vastja
652 70a3df53 vastja
/**
653 c8718599 Martin Sebela
 * Change browser url based on animation step.
654 70a3df53 vastja
 */
655 c8718599 Martin Sebela
const changeUrlParameters = () => {
656
  window.history.pushState(
657
    '',
658
    document.title,
659
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
660
  )
661 03c02899 vastja
}
662
663 c892003d Martin Sebela
664
665 dfe43218 vastja
666 c8718599 Martin Sebela
/* ------------ UTILS ------------ */
667
668
const formatTime = (hours, twoDigitsHours = false) => {
669
  return ((twoDigitsHours && hours < 10) ? '0' : '') + hours + ':00';
670 03c02899 vastja
}
671 0a828a5a Martin Sebela
672 c8718599 Martin Sebela
const formatDate = (date) => {
673 8feb1753 ballakt
  var day = String(date.getDate())
674
  var month = String(date.getMonth() + 1)
675 0a828a5a Martin Sebela
676
  if (day.length === 1) {
677 8feb1753 ballakt
    day = '0' + day
678 0a828a5a Martin Sebela
  }
679
680
  if (month.length === 1) {
681 8feb1753 ballakt
    month = '0' + month
682 0a828a5a Martin Sebela
  }
683
684 c8718599 Martin Sebela
  // return YYYY-MM-DD
685 8feb1753 ballakt
  return date.getFullYear() + '-' + month + '-' + day
686 0a828a5a Martin Sebela
}
687
688 c8718599 Martin Sebela
const currentDayToString = () => {
689
  const day = currentDate.getDate()
690
  return day > 9 ? `${day}` : `0${day}`
691
}
692
693
const currentMonthToString = () => {
694
  const month = currentDate.getMonth() + 1
695
  return month > 9 ? `${month}` : `0${month}`
696
}
697
698
const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
699
700
const addDayToCurrentDate = (day) => {
701
  currentDate.setDate(currentDate.getDate() + day)
702
  changeCurrentDate(currentDate)
703
}
704
705
const areCoordsIdentical = (first, second) => {
706
  return first.lat === second.lat && first.lng === second.lng
707
}
708
709
const debounce = (func, delay) => {
710
  let inDebounce
711
  return function () {
712
    const context = this
713
    const args = arguments
714
    clearTimeout(inDebounce)
715
    inDebounce = setTimeout(() => func.apply(context, args), delay)
716
  }
717
}
718
719
720
721
722
/* ------------ GUI ------------ */
723
724
const updateHeaderControls = () => {
725
  $(`#time_${currentTime}`).prop('checked', true)
726
  $('#dropdownMenuButtonTime').html(formatTime(currentTime, true))
727
}
728
729 8feb1753 ballakt
function initDatepicker (availableDatesSource) {
730
  var availableDates = ''
731 0a828a5a Martin Sebela
732
  $.ajax({
733
    type: 'GET',
734
    url: availableDatesSource,
735 8feb1753 ballakt
    success: function (result) {
736
      availableDates = String(result).split(',')
737 0a828a5a Martin Sebela
    }
738 a7e04778 Martin Sebela
  }).then(function () {
739
    $('#date').datepicker({
740
      format: 'yyyy-mm-dd',
741
      language: 'cs',
742 8feb1753 ballakt
      beforeShowDay: function (date) {
743 a7e04778 Martin Sebela
        if (availableDates.indexOf(formatDate(date)) < 0) {
744 8feb1753 ballakt
          return { enabled: false, tooltip: 'Žádná data' }
745 1774c06d Tomáš Ballák
        } else {
746 8feb1753 ballakt
          return { enabled: true }
747 a7e04778 Martin Sebela
        }
748
      },
749
      autoclose: true
750 8feb1753 ballakt
    })
751
  })
752
}
753 dd652e61 Martin Sebela
754 81980e82 ballakt
function initLocationsMenu () {
755 1a1d8f64 Martin Sebela
  const elmLocationsList = $('.locations')
756
  const locationsDisplayClass = 'show'
757 dd652e61 Martin Sebela
758 81980e82 ballakt
  if ($(window).width() <= 480) {
759 1a1d8f64 Martin Sebela
    elmLocationsList.removeClass(locationsDisplayClass)
760 1774c06d Tomáš Ballák
  } else {
761 1a1d8f64 Martin Sebela
    elmLocationsList.addClass(locationsDisplayClass)
762 dd652e61 Martin Sebela
  }
763 4e003182 Martin Sebela
}
764
765 c8718599 Martin Sebela
const setTimeline = () => {
766
  $('#player-time > span').text(formatTime(currentTime))
767
  $('#player-time').attr('class', 'time hour-' + currentTime)
768
}
769
770
const onValueChangeRegister = () => {
771
  $('#date').change(function () {
772
    changeCurrentDate($(this).val())
773
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
774
    changeUrlParameters()
775 2f227a6c ballakt
  })
776 1cf1413d ballakt
777 c8718599 Martin Sebela
  $('#dropdown-time input[type="radio"]').each(function () {
778
    $(this).change(function () {
779
      changeHour(parseInt($(this).val()))
780
      drawHeatmap(data[currentTime])
781
    })
782
  })
783
784
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
785
    $(this).change(
786
      debounce(() => onCheckboxClicked(this), 1000)
787
    )
788
  })
789
}
790
791
const onCheckboxClicked = async (checkbox) => {
792
  if ($(checkbox).prop('checked')) {
793
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
794
  } else {
795
    loadCheckboxDatasetNameData()
796
797
    data.forEach((item, index) => {
798
      Object.keys(item).forEach(datasetName => {
799
        if (datasetName === $(checkbox).val()) {
800
          delete data[index][datasetName]
801
        }
802
      })
803
804
      drawHeatmap(data[currentTime])
805
    })
806
  }
807
808
  updatePopup()
809
  changeUrlParameters()
810 1cf1413d ballakt
}
811 c892003d Martin Sebela
812 1cf1413d ballakt
const loadCheckboxDatasetNameData = () => {
813 2f227a6c ballakt
  datasetSelected = []
814 1a1d8f64 Martin Sebela
815 c892003d Martin Sebela
  $('#dropdown-dataset .dropdown-item').each(function () {
816 2f227a6c ballakt
    const input = $(this).find('input')
817
    const inputVal = input[0].value
818 c892003d Martin Sebela
819 2f227a6c ballakt
    if (input[0].checked) {
820
      datasetSelected.push(inputVal)
821
    }
822 c892003d Martin Sebela
823 883a423e Tomáš Ballák
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
824 2f227a6c ballakt
  })
825
}
826 ec5e3220 Martin Sebela
827 c8718599 Martin Sebela
function updateAvailableDataSets (available) {
828
  let leastOneOptionEnabled = false
829 ec5e3220 Martin Sebela
830 c8718599 Martin Sebela
  $('#dropdown-dataset .dropdown-item').each(function () {
831
    const input = $(this).find('input')
832 ec5e3220 Martin Sebela
833 c8718599 Martin Sebela
    if (!(input[0].value in available)) {
834
      $(this).addClass('disabled')
835
      $(input).prop('checked', false)
836
    } else {
837
      leastOneOptionEnabled = true
838
      $(this).removeClass('disabled')
839
    }
840
  })
841 815159f3 Tomáš Ballák
842 c8718599 Martin Sebela
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
843
}
844 ec5e3220 Martin Sebela
845 c8718599 Martin Sebela
846
847
848
/* ------------ GUI LOADING ------------ */
849
850
const loadingCallbackNested = (func, delay) => {
851
  setTimeout(() => {
852
    func(loading)
853
    if (loading) {
854
      loadingCallbackNested(func, delay)
855 fdba469a Martin Sebela
    }
856 c8718599 Martin Sebela
  }, delay)
857
}
858
859
const loadingY = (delay = defaultLoaderDelay) => {
860
  loading++
861
  // check after nms if there is something that is loading
862
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
863
}
864
865
const loadingN = (delay = defaultLoaderDelay) => {
866
  loading--
867
  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
868
}
869
870
const loadingTimeline = (isLoading) => {
871
  if (isLoading) {
872
    loadingYTimeline()
873
  } else {
874
    loadingNTimeline()
875 ec5e3220 Martin Sebela
  }
876 815159f3 Tomáš Ballák
}
877 c8718599 Martin Sebela
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
}