commit c87185997ddf942bd64c9a0490cc1c62e3a3aba7
Author: Martin Sebela <msebela@zcu.cz>
Date:   Sat Oct 24 18:59:33 2020 +0200

    Re #8163 - refactoring

diff --git a/website/public/js/zcu-heatmap.js b/website/public/js/zcu-heatmap.js
index 667fe1b..7687fd9 100644
--- a/website/public/js/zcu-heatmap.js
+++ b/website/public/js/zcu-heatmap.js
@@ -3,7 +3,7 @@
 
 var mymap
 var heatmapLayer = null
-var marksLayer = null
+var markersLayer = null
 
 // values for arrow keys
 const arrowKeyLEFT = 37
@@ -66,80 +66,25 @@ const globalPopup = {
   _popup: null
 }
 
-const setGlobalPopupContent = (content) => {
-  globalPopup._popup.setContent(content)
-  globalPopup._popup.openOn(mymap)
-}
-
-const disablePopupControls = () => {
-  $('#btn-previous-page').prop('disabled', true)
-  $('#btn-next-page').prop('disabled', true)
-  $('.popup-controls').hide()
-}
 
-const areCoordsIdentical = (first, second) => {
-  return first.lat === second.lat && first.lng === second.lng
-}
 
-const loadingCallbackNested = (func, delay) => {
-  setTimeout(() => {
-    func(loading)
-    if (loading) {
-      loadingCallbackNested(func, delay)
-    }
-  }, delay)
-}
-
-const loadingY = (delay = defaultLoaderDelay) => {
-  loading++
-  // check after nms if there is something that is loading
-  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
-}
-
-const loadingN = (delay = defaultLoaderDelay) => {
-  loading--
-  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
-}
-
-const changeCurrentTime = (time = null) => {
-  if (time !== null) {
-    currentTime = time
-  } else {
-    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
-  }
-}
 
-const changeCurrentDate = (date = null) => {
-  const dateInput = $('#date')
-  currentDate = new Date(date ? date : dateInput.val())
-
-  dateInput.val(currentDateToString())
-  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
-
-  data = []
-}
+function onDocumentReady () {
+  $('#dropdown-dataset').on('click', function (e) {
+    e.stopPropagation()
+  })
 
-const currentDayToString = () => {
-  const day = currentDate.getDate()
-  return day > 9 ? `${day}` : `0${day}`
+  $('#btn-update-heatmap').prop('name', '')
+  changeCurrentTime()
+  changeCurrentDate()
+  onValueChangeRegister()
+  onArrowLeftRightKeysDownRegister()
 }
 
-const currentMonthToString = () => {
-  const month = currentDate.getMonth() + 1
-  return month > 9 ? `${month}` : `0${month}`
-}
 
-const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
 
-const addDayToCurrentDate = (day) => {
-  currentDate.setDate(currentDate.getDate() + day)
-  changeCurrentDate(currentDate)
-}
 
-const toggleDayLock = () => {
-  lockedDay = !lockedDay
-  $('#player-date').toggleClass('lock')
-}
+/* ------------ DATA FETCHERS ------------ */
 
 const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
   const headers = new Headers()
@@ -163,134 +108,239 @@ const fetchDataSourceMarks = async (positionRoute, datasetName) => {
   return beforeJson.json()
 }
 
-const getPaginationButtonsInPopup = (currentPage, countPages) => ({
-  previousButton: '<button id="btn-previous-page" class="circle-button" onclick="setPreviousPageInPopup()"></button>',
-  pagesList: `<div id="pages">${currentPage} z ${countPages}</div>`,
-  nextButton: '<button id="btn-next-page" class="circle-button next" onclick="setNextPageInPopup()"></button>'
-})
+async function preload (time, change, date) {
+  loadingY()
 
-const genPopUpControls = (controls) => {
-  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
+  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
+    if (!data[nTime]) {
+      data[nTime] = {}
+    }
+
+    datasetSelected.forEach(async (datasetName) => {
+      if (!data[nTime][datasetName]) {
+        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
+      }
+    })
+  }
+
+  loadingN()
 }
 
-const genMultipleDatasetsPopUp = (sum, currentPage, countPages, datasetName) => {
-  const popupHeader = `<strong id="dataset-name">${datasetName}</strong>`
-  const popupData = `<div id="place-intesity"><span id="current-number">${sum}</span></div>`
-  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
+/**
+ * Load and display heatmap layer for current data
+ * 
+ * @param {string} opendataRoute route to dataset source
+ * @param {string} positionsRoute  route to dataset postitions source
+ */
+async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
+  loadCheckboxDatasetNameData()
 
-  return `
-  ${popupHeader}
-  ${popupData}
-  ${genPopUpControls([previousButton, pagesList, nextButton])}
-  `
+  dataSourceRoute = opendataRoute
+  positionsSourceRoute = positionsRoute
+  const allPromises = []
+  data[currentTime] = {}
+
+  const dataSelectedHandler = async (datasetName) => {
+    if (!(datasetName in dataSourceMarks)) {
+      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
+    }
+
+    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
+    data[currentTime][datasetName] = datasetData
+  }
+  datasetSelected.forEach((datasetName) => {
+    allPromises.push(dataSelectedHandler(datasetName))
+  })
+
+  loadingY(loaderDelay)
+
+  await Promise.all(allPromises).then(
+    () => {
+      loadingN(0)
+      drawMapMarkers(dataSourceMarks)
+      drawHeatmap(data[currentTime])
+      
+      preload(currentTime, 1, currentDateToString())
+      preload(currentTime, -1, currentDateToString())
+    }
+  )
 }
 
-const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
-  autoPan: false,
-  className: className
-}).setLatLng([lat / num, lng / num])
+/**
+ * Checks dataset availibility
+ * @param {string} route authority for datasets availibility checks
+ */
+function checkDataSetsAvailability (route) {
+  $.ajax({
+    type: 'POST',
+    // Todo it might be good idea to change db collections format
+    url: route + '/' + currentDateToString(),
+    success: function (result) {
+      updateAvailableDataSets(result)
+    }
+  })
+}
 
-const getPopupContent = (datasetName, placeName, currentCount, sum, currentPage, countPages) => {
-  const popupHeader = `
-    <strong>${datasetName}</strong>
-    <div id="place-name">${placeName}</div>`
-  const popupData = `
-    <div id="place-intesity">
-      <span id="current-number">${currentCount}</span>
-      <span id="part-info">${(sum && sum !== Number(currentCount)) ? '/' + sum : ''}</span>
-    </div>`
-  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
 
-  return `
-  ${popupHeader}
-  ${popupData}
-  ${genPopUpControls(countPages > 1 ? [previousButton, pagesList, nextButton] : null)}
-  `
+
+
+/* ------------ MAP ------------ */
+
+/**
+ * Initialize leaflet map on start position which can be default or set based on user action
+ */
+function initMap () {
+  startX = localStorage.getItem('lat') || startX
+  startY = localStorage.getItem('lng') || startY
+  startZoom = localStorage.getItem('zoom') || startZoom
+
+  mymap = L.map('heatmap').setView([startX, startY], startZoom)
+
+  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+    attribution: '',
+    maxZoom: 19
+  }).addTo(mymap)
+
+  mymap.on('click', function (e) { showInfo(e) })
 }
 
-const onCheckboxClicked = async (checkbox) => {
-  if ($(checkbox).prop('checked')) {
-    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
-  } else {
-    loadCheckboxDatasetNameData()
+function setMapView (latitude, longitude, zoom) {
+  localStorage.setItem('lat', latitude)
+  localStorage.setItem('lng', longitude)
+  localStorage.setItem('zoom', zoom)
 
-    data.forEach((item, index) => {
-      Object.keys(item).forEach(datasetName => {
-        if (datasetName === $(checkbox).val()) {
-          delete data[index][datasetName]
+  mymap.setView([latitude, longitude], zoom)
+}
+
+function drawHeatmap (dataRaw) {
+  // Todo still switched
+  const dataDict = dataRaw
+  const mergedPoints = []
+  let max = 0
+
+  if (Object.keys(globalMarkersChanged).length) {
+    Object.keys(globalMarkersChanged).forEach(function (key) {
+      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
+    })
+    globalMarkersChanged = {}
+  }
+
+  Object.keys(dataDict).forEach((key) => {
+    const data = dataDict[key]
+    max = Math.max(max, data.max)
+
+    if (data != null) {
+    // Bind back popups for markers (we dont know if there is any data for this marker or not)
+      const points = data.items.map((point) => {
+        const { x, y, number } = point
+        const key = x + '' + y
+        const holder = globalMarkersHolder[key]
+        if (!globalMarkersChanged[key] && number) {
+          // There is data for this marker => unbind popup with zero value
+          holder[0] = holder[0].unbindPopup()
+          globalMarkersChanged[key] = holder
         }
+
+        return [x, y, number]
       })
+      mergedPoints.push(...points)
+    } else {
+      if (heatmapLayer != null) {
+        mymap.removeLayer(heatmapLayer)
+      }
+    }
+  })
 
-      drawHeatmap(data[currentTime])
-    })
+  if (heatmapLayer != null) {
+    mymap.removeLayer(heatmapLayer)
   }
 
+  if (mergedPoints.length) {
+    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
+  }
+
+  // timto vyresen bug #8191 - TODO: znamena to, ze muzeme smazat volani updatePopup() ve funkcich, kde se nejdriv vola drawHeatmap() a pak updatePopup()?
   updatePopup()
-  changeUrlParameters()
 }
 
-const onArrowLeftRightKeysDownRegister = () => {
-  $(document).keydown(function (e) {
-    const { which } = e
-    
-    if (which === arrowKeyLEFT) {
-      previous()
-      e.preventDefault()
-    } else if (which === arrowKeyRIGHT) {
-      next()
-      e.preventDefault()
+function drawMapMarkers (data) {
+  if (markersLayer != null) {
+    mymap.removeLayer(markersLayer)
+  }
+
+  markersLayer = L.layerGroup()
+
+  Object.keys(data).forEach(key_ => {
+    for (var key in data[key_]) {
+      const { x, y, name } = data[key_][key]
+      const pop =
+          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
+            .setContent(getPopupContent(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
+      const newCircle =
+        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
+          .bindPopup(pop)
+      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
+      markersLayer.addLayer(
+        newCircle
+      )
     }
   })
+
+  markersLayer.setZIndex(-1).addTo(mymap)
 }
 
-const debounce = (func, delay) => {
-  let inDebounce
-  return function () {
-    const context = this
-    const args = arguments
-    clearTimeout(inDebounce)
-    inDebounce = setTimeout(() => func.apply(context, args), delay)
+
+
+
+/* ------------ GUI ------------ */
+
+const changeCurrentTime = (time = null) => {
+  if (time !== null) {
+    currentTime = time
+  } else {
+    currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
   }
 }
 
-const onValueChangeRegister = () => {
-  $('#date').change(function () {
-    changeCurrentDate($(this).val())
-    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
-    changeUrlParameters()
-  })
+const changeCurrentDate = (date = null) => {
+  const dateInput = $('#date')
+  currentDate = new Date(date ? date : dateInput.val())
 
-  $('#dropdown-time input[type="radio"]').each(function () {
-    $(this).change(function () {
-      changeHour(parseInt($(this).val()))
-      drawHeatmap(data[currentTime])
-    })
-  })
+  dateInput.val(currentDateToString())
+  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
 
-  $('#dropdown-dataset input[type="checkbox"]').each(function () {
-    $(this).change(
-      debounce(() => onCheckboxClicked(this), 1000)
-    )
-  })
+  data = []
 }
 
-/**
- * Initialize leaflet map on start position which can be default or set based on user action
- */
-// eslint-disable-next-line no-unused-vars
-function initMap () {
-  startX = localStorage.getItem('lat') || startX
-  startY = localStorage.getItem('lng') || startY
-  startZoom = localStorage.getItem('zoom') || startZoom
+const toggleDayLock = () => {
+  lockedDay = !lockedDay
+  $('#player-date').toggleClass('lock')
+}
 
-  mymap = L.map('heatmap').setView([startX, startY], startZoom)
 
-  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-    attribution: '',
-    maxZoom: 19
-  }).addTo(mymap)
 
-  mymap.on('click', function (e) { showInfo(e) })
+
+/* ------------ POPUPs ------------ */
+
+const setGlobalPopupContent = (content) => {
+  globalPopup._popup.setContent(content)
+  globalPopup._popup.openOn(mymap)
+}
+
+const getPaginationButtonsInPopup = (currentPage, countPages) => ({
+  previousButton: '<button id="btn-previous-page" class="circle-button" onclick="setPreviousPageInPopup()"></button>',
+  pagesList: `<div id="pages">${currentPage} z ${countPages}</div>`,
+  nextButton: '<button id="btn-next-page" class="circle-button next" onclick="setNextPageInPopup()"></button>'
+})
+
+const disablePopupPaginationButtons = () => {
+  $('#btn-previous-page').prop('disabled', true)
+  $('#btn-next-page').prop('disabled', true)
+  $('.popup-controls').hide()
+}
+
+const generatePopupPaginationButtons = (controls) => {
+  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
 }
 
 const getCountPagesInPopup = () => {
@@ -309,6 +359,57 @@ const getPopupDataOnPage = (pageInPopup) => {
   return info[keys[pageInPopup]]
 }
 
+function setPreviousPageInPopup () {
+  const countPagesInPopup = getCountPagesInPopup()
+  const page = currentPageInPopup
+
+  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
+  setPageContentInPopup(page)
+}
+
+function setNextPageInPopup () {
+  const countPagesInPopup = getCountPagesInPopup()
+  const page = currentPageInPopup
+
+  currentPageInPopup = (currentPageInPopup + 1) % countPagesInPopup
+  setPageContentInPopup(page)
+}
+
+const genMultipleDatasetsPopUp = (sum, currentPage, countPages, datasetName) => {
+  const popupHeader = `<strong id="dataset-name">${datasetName}</strong>`
+  const popupData = `<div id="place-intesity"><span id="current-number">${sum}</span></div>`
+  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
+
+  return `
+  ${popupHeader}
+  ${popupData}
+  ${generatePopupPaginationButtons([previousButton, pagesList, nextButton])}
+  `
+}
+
+const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
+  autoPan: false,
+  className: className
+}).setLatLng([lat / num, lng / num])
+
+const getPopupContent = (datasetName, placeName, currentCount, sum, currentPage, countPages) => {
+  const popupHeader = `
+    <strong>${datasetName}</strong>
+    <div id="place-name">${placeName}</div>`
+  const popupData = `
+    <div id="place-intesity">
+      <span id="current-number">${currentCount}</span>
+      <span id="part-info">${(sum && sum !== Number(currentCount)) ? '/' + sum : ''}</span>
+    </div>`
+  const { previousButton, nextButton, pagesList } = getPaginationButtonsInPopup(currentPage, countPages)
+
+  return `
+  ${popupHeader}
+  ${popupData}
+  ${generatePopupPaginationButtons(countPages > 1 ? [previousButton, pagesList, nextButton] : null)}
+  `
+}
+
 const areMultipleDatasetsInRadius = () => {
   return Object.keys(info).length > 1
 }
@@ -379,7 +480,7 @@ function showInfo (e) {
     if (mymap._popup) {
       $('#part-info').text('')
       $('#current-number').html(0)
-      disablePopupControls()
+      disablePopupPaginationButtons()
     }
 
     return
@@ -401,7 +502,7 @@ function showInfo (e) {
     setGlobalPopupContent(getPopupContent(datasetDictNameDisplayName[markersInRadius.datasetName], place, number, total, 1, popupPagesData.length))
 
     if (popupPagesData.length === 1) {
-      disablePopupControls()
+      disablePopupPaginationButtons()
     }
   } else {
     const { datasetName, number } = getPopupDataOnPage(currentPageInPopup)
@@ -418,24 +519,6 @@ function showInfo (e) {
   }
 }
 
-// eslint-disable-next-line no-unused-vars
-function setPreviousPageInPopup () {
-  const countPagesInPopup = getCountPagesInPopup()
-  const page = currentPageInPopup
-
-  currentPageInPopup = (currentPageInPopup + countPagesInPopup - 1) % countPagesInPopup
-  setPageContentInPopup(page)
-}
-
-// eslint-disable-next-line no-unused-vars
-function setNextPageInPopup () {
-  const countPagesInPopup = getCountPagesInPopup()
-  const page = currentPageInPopup
-
-  currentPageInPopup = (currentPageInPopup + 1) % countPagesInPopup
-  setPageContentInPopup(page)
-}
-
 function setPageContentInPopup (page) {
   const previousPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(page) : getPopupDataOnPage(0).items[page]
   const currentPageData = areMultipleDatasetsInRadius() ? getPopupDataOnPage(currentPageInPopup) : getPopupDataOnPage(0).items[currentPageInPopup]
@@ -452,19 +535,24 @@ function setPageContentInPopup (page) {
   $('.leaflet-popup').removeClass(`popup-${previousPageData.datasetName}`).addClass(`popup-${currentPageData.datasetName}`)
 }
 
-// eslint-disable-next-line no-unused-vars
-function setMapView (latitude, longitude, zoom) {
-  localStorage.setItem('lat', latitude)
-  localStorage.setItem('lng', longitude)
-  localStorage.setItem('zoom', zoom)
+const updatePopup = () => {
+  const { _popup } = mymap
 
-  mymap.setView([latitude, longitude], zoom)
+  if (_popup) {
+    showInfo({
+      latlng: _popup.getLatLng()
+    })
+  }
 }
 
+
+
+
+/* ------------ ANIMATION ------------ */
+
 /**
  * Change animation start from playing to stopped or the other way round
  */
-// eslint-disable-next-line no-unused-vars
 function changeAnimationState () {
   const btnAnimate = $('#animate-btn')
 
@@ -479,7 +567,6 @@ function changeAnimationState () {
   }
 }
 
-// eslint-disable-next-line no-unused-vars
 async function previous () {
   if (loading) {
     return
@@ -516,243 +603,73 @@ async function next () {
   updatePopup()
 }
 
-/**
- * Change browser url based on animation step.
- */
-const changeUrlParameters = () => {
-  window.history.pushState(
-    '',
-    document.title,
-    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
-  )
-}
-
-const formatTime = (hours, twoDigitsHours = false) => {
-  return ((twoDigitsHours && hours < 10) ? '0' : '') + hours + ':00';
-}
-
-const updateHeaderControls = () => {
-  $(`#time_${currentTime}`).prop('checked', true)
-  $('#dropdownMenuButtonTime').html(formatTime(currentTime, true))
-}
-
-const setTimeline = () => {
-  $('#player-time > span').text(formatTime(currentTime))
-  $('#player-time').attr('class', 'time hour-' + currentTime)
-}
-
-const loadingTimeline = (isLoading) => {
-  if (isLoading) {
-    loadingYTimeline()
-  } else {
-    loadingNTimeline()
-  }
-}
-
-const loadingYTimeline = () => {
-  $('#player-time > .spinner-border').removeClass('d-none')
-  $('#player-time > span').text('')
-}
-
-const loadingNTimeline = () => {
-  $('#player-time > .spinner-border').addClass('d-none')
-  setTimeline()
-}
-
 const onChangeHour = (hour) => {
   changeHour(hour)
   drawHeatmap(data[currentTime])
 }
 
 const changeHour = (hour) => {
-  $('#player-time').removeAttr('style')
-  changeCurrentTime(hour)
-  updateHeaderControls()
-  setTimeline()
-  changeUrlParameters()
-  updatePopup()
-}
-
-const updatePopup = () => {
-  const { _popup } = mymap
-
-  if (_popup) {
-    showInfo({
-      latlng: _popup.getLatLng()
-    })
-  }
-}
-
-/**
- * Load and display heatmap layer for current data
- * @param {string} opendataRoute route to dataset source
- * @param {string} positionsRoute  route to dataset postitions source
- */
-// eslint-disable-next-line no-unused-vars
-async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute, loaderDelay = defaultLoaderDelay) {
-  loadCheckboxDatasetNameData()
-
-  dataSourceRoute = opendataRoute
-  positionsSourceRoute = positionsRoute
-  const allPromises = []
-  data[currentTime] = {}
-
-  const dataSelectedHandler = async (datasetName) => {
-    if (!(datasetName in dataSourceMarks)) {
-      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
-    }
-
-    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
-    data[currentTime][datasetName] = datasetData
-  }
-  datasetSelected.forEach((datasetName) => {
-    allPromises.push(dataSelectedHandler(datasetName))
-  })
-
-  loadingY(loaderDelay)
-
-  await Promise.all(allPromises).then(
-    () => {
-      loadingN(0)
-      drawDataSourceMarks(dataSourceMarks)
-      drawHeatmap(data[currentTime])
-      preload(currentTime, 1, currentDateToString())
-      preload(currentTime, -1, currentDateToString())
-    }
-  )
-}
-
-function drawDataSourceMarks (data) {
-  if (marksLayer != null) {
-    mymap.removeLayer(marksLayer)
-  }
-
-  marksLayer = L.layerGroup()
-
-  Object.keys(data).forEach((key_) => {
-    for (var key in data[key_]) {
-      const { x, y, name } = data[key_][key]
-      const pop =
-          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
-            .setContent(getPopupContent(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
-      const newCircle =
-        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
-          .bindPopup(pop)
-      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
-      marksLayer.addLayer(
-        newCircle
-      )
-    }
-  })
-
-  marksLayer.setZIndex(-1).addTo(mymap)
-}
-
-async function preload (time, change, date) {
-  loadingY()
-
-  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
-    if (!data[nTime]) {
-      data[nTime] = {}
-    }
-
-    datasetSelected.forEach(async (datasetName) => {
-      if (!data[nTime][datasetName]) {
-        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
-      }
-    })
-  }
-
-  loadingN()
+  $('#player-time').removeAttr('style')
+  changeCurrentTime(hour)
+  updateHeaderControls()
+  setTimeline()
+  changeUrlParameters()
+  updatePopup()
 }
 
-function drawHeatmap (dataRaw) {
-  // Todo still switched
-  const dataDict = dataRaw
-  const mergedPoints = []
-  let max = 0
+const dragTimeline = () => {
+  const hourElemWidthPx = 26
 
-  if (Object.keys(globalMarkersChanged).length) {
-    Object.keys(globalMarkersChanged).forEach(function (key) {
-      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
-    })
-    globalMarkersChanged = {}
-  }
+  const elem = $('#player-time')
+  const offset = elem.offset().left - elem.parent().offset().left
 
-  Object.keys(dataDict).forEach((key) => {
-    const data = dataDict[key]
-    max = Math.max(max, data.max)
+  if (offset >= 0 && offset <= elem.parent().width()) {
+    const hour = Math.round(offset / hourElemWidthPx)
 
-    if (data != null) {
-    // Bind back popups for markers (we dont know if there is any data for this marker or not)
-      const points = data.items.map((point) => {
-        const { x, y, number } = point
-        const key = x + '' + y
-        const holder = globalMarkersHolder[key]
-        if (!globalMarkersChanged[key] && number) {
-          // There is data for this marker => unbind popup with zero value
-          holder[0] = holder[0].unbindPopup()
-          globalMarkersChanged[key] = holder
-        }
+    if (hour !== currentTime) {
+      elem.attr('class', 'time hour-' + hour)
+      $('#player-time span').html(formatTime(hour))
 
-        return [x, y, number]
-      })
-      mergedPoints.push(...points)
-    } else {
-      if (heatmapLayer != null) {
-        mymap.removeLayer(heatmapLayer)
-      }
+      onChangeHour(hour)
     }
-  })
-
-  if (heatmapLayer != null) {
-    mymap.removeLayer(heatmapLayer)
-  }
-
-  if (mergedPoints.length) {
-    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
   }
+}
 
-  // timto vyresen bug #8191 - TODO: znamena to, ze muzeme smazat volani updatePopup() ve funkcich, kde se nejdriv vola drawHeatmap() a pak updatePopup()?
-  updatePopup()
+const onArrowLeftRightKeysDownRegister = () => {
+  $(document).keydown(function (e) {
+    const { which } = e
+    
+    if (which === arrowKeyLEFT) {
+      previous()
+      e.preventDefault()
+    } else if (which === arrowKeyRIGHT) {
+      next()
+      e.preventDefault()
+    }
+  })
 }
 
 /**
- * Checks dataset availibility
- * @param {string} route authority for datasets availibility checks
+ * Change browser url based on animation step.
  */
-// eslint-disable-next-line no-unused-vars
-function checkDataSetsAvailability (route) {
-  $.ajax({
-    type: 'POST',
-    // Todo it might be good idea to change db collections format
-    url: route + '/' + currentDateToString(),
-    success: function (result) {
-      updateAvailableDataSets(result)
-    }
-  })
+const changeUrlParameters = () => {
+  window.history.pushState(
+    '',
+    document.title,
+    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
+  )
 }
 
-function updateAvailableDataSets (available) {
-  let leastOneOptionEnabled = false
 
-  $('#dropdown-dataset .dropdown-item').each(function () {
-    const input = $(this).find('input')
-    const inputVal = input[0].value
 
-    if (!(inputVal in available)) {
-      $(this).addClass('disabled')
-      $(input).prop('checked', false)
-    } else {
-      leastOneOptionEnabled = true
-      $(this).removeClass('disabled')
-    }
-  })
 
-  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
+/* ------------ UTILS ------------ */
+
+const formatTime = (hours, twoDigitsHours = false) => {
+  return ((twoDigitsHours && hours < 10) ? '0' : '') + hours + ':00';
 }
 
-function formatDate (date) {
+const formatDate = (date) => {
   var day = String(date.getDate())
   var month = String(date.getMonth() + 1)
 
@@ -764,10 +681,51 @@ function formatDate (date) {
     month = '0' + month
   }
 
+  // return YYYY-MM-DD
   return date.getFullYear() + '-' + month + '-' + day
 }
 
-// eslint-disable-next-line no-unused-vars
+const currentDayToString = () => {
+  const day = currentDate.getDate()
+  return day > 9 ? `${day}` : `0${day}`
+}
+
+const currentMonthToString = () => {
+  const month = currentDate.getMonth() + 1
+  return month > 9 ? `${month}` : `0${month}`
+}
+
+const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
+
+const addDayToCurrentDate = (day) => {
+  currentDate.setDate(currentDate.getDate() + day)
+  changeCurrentDate(currentDate)
+}
+
+const areCoordsIdentical = (first, second) => {
+  return first.lat === second.lat && first.lng === second.lng
+}
+
+const debounce = (func, delay) => {
+  let inDebounce
+  return function () {
+    const context = this
+    const args = arguments
+    clearTimeout(inDebounce)
+    inDebounce = setTimeout(() => func.apply(context, args), delay)
+  }
+}
+
+
+
+
+/* ------------ GUI ------------ */
+
+const updateHeaderControls = () => {
+  $(`#time_${currentTime}`).prop('checked', true)
+  $('#dropdownMenuButtonTime').html(formatTime(currentTime, true))
+}
+
 function initDatepicker (availableDatesSource) {
   var availableDates = ''
 
@@ -804,16 +762,51 @@ function initLocationsMenu () {
   }
 }
 
-function onDocumentReady () {
-  $('#dropdown-dataset').on('click', function (e) {
-    e.stopPropagation()
+const setTimeline = () => {
+  $('#player-time > span').text(formatTime(currentTime))
+  $('#player-time').attr('class', 'time hour-' + currentTime)
+}
+
+const onValueChangeRegister = () => {
+  $('#date').change(function () {
+    changeCurrentDate($(this).val())
+    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
+    changeUrlParameters()
   })
 
-  $('#btn-update-heatmap').prop('name', '')
-  changeCurrentTime()
-  changeCurrentDate()
-  onValueChangeRegister()
-  onArrowLeftRightKeysDownRegister()
+  $('#dropdown-time input[type="radio"]').each(function () {
+    $(this).change(function () {
+      changeHour(parseInt($(this).val()))
+      drawHeatmap(data[currentTime])
+    })
+  })
+
+  $('#dropdown-dataset input[type="checkbox"]').each(function () {
+    $(this).change(
+      debounce(() => onCheckboxClicked(this), 1000)
+    )
+  })
+}
+
+const onCheckboxClicked = async (checkbox) => {
+  if ($(checkbox).prop('checked')) {
+    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute, 0)
+  } else {
+    loadCheckboxDatasetNameData()
+
+    data.forEach((item, index) => {
+      Object.keys(item).forEach(datasetName => {
+        if (datasetName === $(checkbox).val()) {
+          delete data[index][datasetName]
+        }
+      })
+
+      drawHeatmap(data[currentTime])
+    })
+  }
+
+  updatePopup()
+  changeUrlParameters()
 }
 
 const loadCheckboxDatasetNameData = () => {
@@ -831,20 +824,63 @@ const loadCheckboxDatasetNameData = () => {
   })
 }
 
-const dragTimeline = () => {
-  const hourElemWidthPx = 26
+function updateAvailableDataSets (available) {
+  let leastOneOptionEnabled = false
 
-  const elem = $('#player-time')
-  const offset = elem.offset().left - elem.parent().offset().left
+  $('#dropdown-dataset .dropdown-item').each(function () {
+    const input = $(this).find('input')
 
-  if (offset >= 0 && offset <= elem.parent().width()) {
-    const hour = Math.round(offset / hourElemWidthPx)
+    if (!(input[0].value in available)) {
+      $(this).addClass('disabled')
+      $(input).prop('checked', false)
+    } else {
+      leastOneOptionEnabled = true
+      $(this).removeClass('disabled')
+    }
+  })
 
-    if (hour !== currentTime) {
-      elem.attr('class', 'time hour-' + hour)
-      $('#player-time span').html(formatTime(hour))
+  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
+}
 
-      onChangeHour(hour)
+
+
+
+/* ------------ GUI LOADING ------------ */
+
+const loadingCallbackNested = (func, delay) => {
+  setTimeout(() => {
+    func(loading)
+    if (loading) {
+      loadingCallbackNested(func, delay)
     }
+  }, delay)
+}
+
+const loadingY = (delay = defaultLoaderDelay) => {
+  loading++
+  // check after nms if there is something that is loading
+  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading), delay))
+}
+
+const loadingN = (delay = defaultLoaderDelay) => {
+  loading--
+  loadingCallbackNested(() => loadingCallbackNested((isLoading) => loadingTimeline(isLoading)), delay)
+}
+
+const loadingTimeline = (isLoading) => {
+  if (isLoading) {
+    loadingYTimeline()
+  } else {
+    loadingNTimeline()
   }
 }
+
+const loadingYTimeline = () => {
+  $('#player-time > .spinner-border').removeClass('d-none')
+  $('#player-time > span').text('')
+}
+
+const loadingNTimeline = () => {
+  $('#player-time > .spinner-border').addClass('d-none')
+  setTimeline()
+}
\ No newline at end of file
