Projekt

Obecné

Profil

Stáhnout (14.9 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
var marksLayer = null
7 03c02899 vastja
8 8feb1753 ballakt
var startX = 49.7248
9
var startY = 13.3521
10
var startZoom = 17
11 3fc08f2d vastja
12 8feb1753 ballakt
var dataSourceRoute
13
var currentTime
14 a48642fb vastja
15 8feb1753 ballakt
var timer
16
var isAnimationRunning = false
17
var data = []
18 a48642fb vastja
19 70a3df53 vastja
var info = []
20 2f227a6c ballakt
let currentInfo = 0
21
22
var datasetSelected = []
23 70a3df53 vastja
24 8feb1753 ballakt
const globalMarkersHolder = {}
25 2f227a6c ballakt
const datasetColorDict = {}
26 084a5972 ballakt
27 2f227a6c ballakt
/* const genColor = (datasetNames) => {
28
  datasetNames.forEach((name) => {
29
    datasetColorDict[name] =
30
  })
31
} */
32
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
33
  const headers = new Headers()
34
  const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, {
35
    method: 'GET',
36
    headers: headers
37
  })
38
  const beforeJson = await fetch(myRequest)
39
  return beforeJson.json()
40
}
41
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
42
  const headers = new Headers()
43
  const myRequest = new Request(positionRoute + '/' + datasetName, {
44
    method: 'GET',
45
    headers: headers
46
  })
47
  const beforeJson = await fetch(myRequest)
48
  return beforeJson.json()
49
}
50 8feb1753 ballakt
// all marker from which popup was removed
51 084a5972 ballakt
// contains: {key:[L.circle,L.pupup]}
52
// key: x and y, x + '' + y string
53 8feb1753 ballakt
let globalMarkersChanged = {}
54 084a5972 ballakt
55 2f227a6c ballakt
const genPopUpControlButtons = (currentPage, numPages, onNextClick, onPreviousClick) => ({
56
  previousButton: '<button id="previous-info-btn" class="circle-button" onclick="previousInfo()"></button>',
57
  nextButton: '<button id="next-info-btn" onclick="nextInfo()" class="circle-button next"></button>',
58
  posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>`
59
})
60 084a5972 ballakt
const genPopUpControls = (controls) => {
61 2f227a6c ballakt
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
62 084a5972 ballakt
}
63 2f227a6c ballakt
const multipleDatasetsPopUp = (sum, currentPos, maxPos, datasetName) => {
64
  const header = `<strong>Dataset a počet:</strong><div id="place-info">${datasetName}</div>`
65
  const digitInfo = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
66
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
67
  return `
68
  ${header}
69
  ${digitInfo}
70
  ${genPopUpControls([previousButton, posInfo, nextButton])}
71
  `
72
}
73
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
74
  autoPan: false,
75
  className: className
76
}).setLatLng([lat / num, lng / num])
77
78 084a5972 ballakt
const genPopUp = (place, number, sum, currentPos, maxPos) => {
79 8feb1753 ballakt
  const header = `<strong>Zařízení a počet:</strong><div id="place-info">${place}</div>`
80
  const currentNum = `<span id="digit-info">${number}</span>`
81 90d3db28 Tomáš Ballák
  // eslint-disable-next-line eqeqeq
82
  const sumNum = `<span id="total-info" style="font-size: large">${(sum && (sum != number)) ? '/' + sum : ''}</span>`
83 8feb1753 ballakt
  const digitInfo = `<div id="number-info">${currentNum}${sumNum}</div>`
84 2f227a6c ballakt
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
85 084a5972 ballakt
  return `
86
  ${header}
87
  ${digitInfo}
88 2f227a6c ballakt
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
89 084a5972 ballakt
  `
90
}
91 70a3df53 vastja
/**
92
 * Initialize leaflet map on start position which can be default or set based on user action
93
 */
94 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
95
function initMap () {
96 90d3db28 Tomáš Ballák
  startX = localStorage.getItem('lat') || startX
97
  startY = localStorage.getItem('lng') || startY
98
  startZoom = localStorage.getItem('zoom') || startZoom
99 72a438f3 vastja
100 8feb1753 ballakt
  mymap = L.map('heatmap').setView([startX, startY], startZoom)
101 3fc08f2d vastja
102 c236b33a msebela
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
103
    attribution: '',
104
    maxZoom: 19
105 8feb1753 ballakt
  }).addTo(mymap)
106 3ae59f75 vastja
107 8feb1753 ballakt
  mymap.on('click', showInfo)
108 c236b33a msebela
}
109 2f227a6c ballakt
const getInfoLength = () => {
110
  const infoKeys = Object.keys(info)
111
  if (infoKeys.length === 1) {
112
    // return number of records in one dataset (one dataset in area)
113
    return info[infoKeys[0]].items.length
114
  }
115
  // return number of datasets (agregation of all datasets in area)
116
  return infoKeys.length
117
}
118
const getElFromObjectInfo = (position) => {
119
  const keys = Object.keys(info)
120
  return info[keys[position]]
121
}
122
const hasInfoMultipleDatasets = () => {
123
  return Object.keys(info).length > 1
124
}
125 8feb1753 ballakt
function showInfo (e) {
126 3ae59f75 vastja
  info = []
127 2f227a6c ballakt
  currentInfo = 0
128 3ae59f75 vastja
129
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
130
  // Todo change to variable - it is used in heatmap init
131 2f227a6c ballakt
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
132
  const radius = 25 * stile / 256
133
134
  let i = 0
135
  let lat = 0
136
  let lng = 0
137
138
  let total = 0
139
140
  const datasetsInRadius = {}
141
142
  Object.keys(data[currentTime]).forEach((key) => {
143
    const namedData = data[currentTime][key]
144
    namedData.items.forEach(element => {
145
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
146
        lat += element.x
147
        lng += element.y
148
        info[i] = { place: element.place, number: element.number, datasetName: key }
149
        total += parseInt(element.number)
150
        i++
151
        datasetsInRadius[key] = true
152
      }
153
    })
154 8feb1753 ballakt
  })
155 3ae59f75 vastja
156 2f227a6c ballakt
  // Process info for more then one dataset
157 8feb1753 ballakt
158 2f227a6c ballakt
  info = info.reduce((acc, item) => {
159
    if (!acc[item.datasetName]) {
160
      acc[item.datasetName] = {
161
        items: [],
162
        number: 0,
163
        datasetName: item.datasetName
164
      }
165
    }
166
    acc[item.datasetName].items.push(item)
167
    acc[item.datasetName].number += Number(item.number)
168
    return acc
169
  }, {})
170
171
  // There is one dataset
172
173
  const numDatasets = Object.keys(datasetsInRadius).length
174
175
  if (!numDatasets) { return }
176
177
  if (numDatasets === 1) {
178
    const infoDict = getElFromObjectInfo(0)
179
    const info_ = infoDict.items
180
    const { place, number } = info_[currentInfo]
181
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
182
      .setContent(genPopUp(place, number, total, currentInfo + 1, info_.length))
183
      .openOn(mymap)
184
    if (info_.length === 1) {
185 8feb1753 ballakt
      $('#previous-info-btn').prop('disabled', true)
186
      $('#next-info-btn').prop('disabled', true)
187
      $('.popup-controls').hide()
188 3ae59f75 vastja
    }
189 2f227a6c ballakt
  } else {
190
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
191
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
192
      .setContent(multipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetName))
193
      .openOn(mymap)
194 3ae59f75 vastja
  }
195
}
196
197 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
198
function previousInfo () {
199 2f227a6c ballakt
  const infoLength = getInfoLength()
200
  const previousCurrentInfo = currentInfo
201
  currentInfo = (currentInfo + infoLength - 1) % infoLength
202
  displayInfoText(previousCurrentInfo)
203 3ae59f75 vastja
}
204
205 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
206
function nextInfo () {
207 2f227a6c ballakt
  const infoLength = getInfoLength()
208
  const previousCurrentInfo = currentInfo
209
  currentInfo = (currentInfo + 1) % infoLength
210
  displayInfoText(previousCurrentInfo)
211 3ae59f75 vastja
}
212 2f227a6c ballakt
function displayInfoText (previousInfoNum) {
213
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
214
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
215
  const infoLength = getInfoLength()
216
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
217
  $('#digit-info').html(info_.number)
218
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
219
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
220
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
221 3ae59f75 vastja
}
222 351696d5 Martin Sebela
223 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
224 72a438f3 vastja
function setMapView (latitude, longitude, zoom) {
225 90d3db28 Tomáš Ballák
  localStorage.setItem('lat', latitude)
226
  localStorage.setItem('lng', longitude)
227
  localStorage.setItem('zoom', zoom)
228 8feb1753 ballakt
  mymap.setView([latitude, longitude], zoom)
229 3fc08f2d vastja
}
230
231 70a3df53 vastja
/**
232
 * Change animation start from playing to stopped or the other way round
233
 */
234 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
235
function changeAnimationState () {
236 a48642fb vastja
  isAnimationRunning = !isAnimationRunning
237
  if (isAnimationRunning) {
238 8feb1753 ballakt
    $('#play-pause').attr('class', 'pause')
239 a48642fb vastja
    timer = setInterval(
240 8feb1753 ballakt
      function () {
241
        next()
242 a48642fb vastja
      },
243
      800
244 8feb1753 ballakt
    )
245
  } else {
246
    clearTimeout(timer)
247
    $('#play-pause').attr('class', 'play')
248 351696d5 Martin Sebela
  }
249
}
250
251 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
252
function previous () {
253
  currentTime = (currentTime + 23) % 24
254
  drawHeatmap(data[currentTime])
255
  setTimeline()
256
  mymap.closePopup()
257
  updateHeaderControls()
258
  changeUrl()
259 a48642fb vastja
}
260
261 8feb1753 ballakt
function next () {
262
  currentTime = (currentTime + 1) % 24
263
  drawHeatmap(data[currentTime])
264
  setTimeline()
265
  mymap.closePopup()
266
  updateHeaderControls()
267
  changeUrl()
268 8b840eb7 vastja
}
269 2f227a6c ballakt
const typeUrlReducer = (accumulator, currentValue) => accumulator + currentValue
270 70a3df53 vastja
/**
271
 * Change browser url based on animation step
272
 */
273 8feb1753 ballakt
function changeUrl () {
274 8b840eb7 vastja
  window.history.pushState(
275 8feb1753 ballakt
    '',
276 8b840eb7 vastja
    document.title,
277 2f227a6c ballakt
    window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
278 8feb1753 ballakt
  )
279 4e8c0e5b Martin Sebela
}
280
281 8feb1753 ballakt
function updateHeaderControls () {
282
  document.getElementById('time').value = currentTime
283 a48642fb vastja
}
284 351696d5 Martin Sebela
285 8feb1753 ballakt
function setTimeline () {
286
  $('#timeline').text(currentTime + ':00')
287
  $('#timeline').attr('class', 'time hour-' + currentTime)
288 351696d5 Martin Sebela
}
289
290 70a3df53 vastja
/**
291
 * Load and display heatmap layer for current data
292
 * @param {string} opendataRoute route to dataset source
293
 * @param {string} positionsRoute  route to dataset postitions source
294
 */
295 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
296 2f227a6c ballakt
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
297 8feb1753 ballakt
  dataSourceRoute = opendataRoute
298
  data = []
299 2f227a6c ballakt
  const dataSourceMarks = {}
300
  const allPromises = []
301
  const date = $('#date').val()
302 8feb1753 ballakt
  currentTime = parseInt($('#time').children('option:selected').val())
303
  setTimeline()
304 2f227a6c ballakt
  data[currentTime] = {}
305
  const dataSelectedHandler = async (datasetName) => {
306
    const marks = await fetchDataSourceMarks(positionsRoute, datasetName)
307
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, date, currentTime)
308
    dataSourceMarks[datasetName] = marks
309
    data[currentTime][datasetName] = datasetData
310
  }
311
  await datasetSelected.forEach((datasetName) => {
312
    allPromises.push(dataSelectedHandler(datasetName))
313 8feb1753 ballakt
  })
314 2f227a6c ballakt
  Promise.all(allPromises).then(
315
    () => {
316
      drawDataSourceMarks(dataSourceMarks)
317
      drawHeatmap(data[currentTime])
318
      preload(currentTime, 1, date)
319
      preload(currentTime, -1, date)
320
    }
321
  )
322 a48642fb vastja
}
323
324 8feb1753 ballakt
function drawDataSourceMarks (data) {
325 61ff7718 vastja
  if (marksLayer != null) {
326 8feb1753 ballakt
    L.removeLayer(marksLayer)
327 61ff7718 vastja
  }
328 8feb1753 ballakt
  marksLayer = L.layerGroup()
329 2f227a6c ballakt
  Object.keys(data).forEach((key_) => {
330
    for (var key in data[key_]) {
331
      const { x, y, name } = data[key_][key]
332
      const pop =
333
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
334
            .setContent(genPopUp(name, 0, 0, 1, 1))
335
      const newCircle =
336
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
337
          .bindPopup(pop)
338
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
339
      marksLayer.addLayer(
340
        newCircle
341
      )
342
    }
343
  })
344 61ff7718 vastja
345 8feb1753 ballakt
  marksLayer.setZIndex(-1).addTo(mymap)
346 61ff7718 vastja
}
347
348 2f227a6c ballakt
async function preload (time, change, date) {
349
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
350
    if (!data[nTime]) {
351
      data[nTime] = {}
352
      datasetSelected.forEach(async (datasetName) => {
353
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
354
      })
355
    }
356 a48642fb vastja
  }
357 3fc08f2d vastja
}
358
359 2f227a6c ballakt
function drawHeatmap (dataRaw) {
360 03c02899 vastja
  // Todo still switched
361 2f227a6c ballakt
  const dataDict = dataRaw
362
  const mergedPoints = []
363
  let max = 0
364
  Object.keys(dataDict).forEach((key) => {
365
    const data = dataDict[key]
366
    max = Math.max(max, data.max)
367
    if (data != null) {
368 8feb1753 ballakt
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
369 2f227a6c ballakt
      if (Object.keys(globalMarkersChanged).length) {
370
        Object.keys(globalMarkersChanged).forEach(function (key) {
371
          globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
372
        })
373
        globalMarkersChanged = {}
374
      }
375
      const points = data.items.map((point) => {
376
        const { x, y, number } = point
377
        const key = x + '' + y
378
        const holder = globalMarkersHolder[key]
379
        if (!globalMarkersChanged[key] && number) {
380 8feb1753 ballakt
        // There is data for this marker => unbind popup with zero value
381 2f227a6c ballakt
          holder[0] = holder[0].unbindPopup()
382
          globalMarkersChanged[key] = holder
383
        }
384
        return [x, y, number]
385
      })
386
      mergedPoints.push(...points)
387
    } else {
388
      if (heatmapLayer != null) {
389
        mymap.removeLayer(heatmapLayer)
390 084a5972 ballakt
      }
391 a48642fb vastja
    }
392 2f227a6c ballakt
  })
393
  if (heatmapLayer != null) {
394
    mymap.removeLayer(heatmapLayer)
395
  }
396
  if (mergedPoints.length) {
397
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
398 a48642fb vastja
  }
399 03c02899 vastja
}
400 3fc08f2d vastja
401 70a3df53 vastja
/**
402
 * Checks dataset availibility
403 81980e82 ballakt
 * @param {string} route authority for datasets availibility checks
404 70a3df53 vastja
 */
405 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
406
function checkDataSetsAvailability (route) {
407 03c02899 vastja
  $.ajax({
408 8feb1753 ballakt
    type: 'POST',
409 03c02899 vastja
    // Todo it might be good idea to change db collections format
410 03ccdd65 vastja
    url: route + '/' + $('#date').val(),
411 8feb1753 ballakt
    success: function (result) {
412
      updateAvailableDataSets(result)
413 03c02899 vastja
    }
414 8feb1753 ballakt
  })
415 03c02899 vastja
}
416
417 8feb1753 ballakt
function updateAvailableDataSets (available) {
418 2f227a6c ballakt
  let leastOneOptionEnabled = false
419
  // datasetSelected = []
420
  $('#dataset-dropdown .dropdown-item').each(function () {
421
    const input = $(this).find('input')
422
    const inputVal = input[0].value
423
    if (!(inputVal in available)) {
424
      $(this).addClass('disabled')
425
      $(input).prop('checked', false)
426 8feb1753 ballakt
    } else {
427 2f227a6c ballakt
      leastOneOptionEnabled = true
428
      $(this).removeClass('disabled')
429 5d599617 vastja
    }
430 8feb1753 ballakt
  })
431 dfe43218 vastja
432 2f227a6c ballakt
  $('#submit-btn').prop('disabled', !leastOneOptionEnabled)
433 03c02899 vastja
}
434 0a828a5a Martin Sebela
435 8feb1753 ballakt
function formatDate (date) {
436
  var day = String(date.getDate())
437
  var month = String(date.getMonth() + 1)
438 0a828a5a Martin Sebela
439
  if (day.length === 1) {
440 8feb1753 ballakt
    day = '0' + day
441 0a828a5a Martin Sebela
  }
442
443
  if (month.length === 1) {
444 8feb1753 ballakt
    month = '0' + month
445 0a828a5a Martin Sebela
  }
446
447 8feb1753 ballakt
  return date.getFullYear() + '-' + month + '-' + day
448 0a828a5a Martin Sebela
}
449
450 8feb1753 ballakt
// eslint-disable-next-line no-unused-vars
451
function initDatepicker (availableDatesSource) {
452
  var availableDates = ''
453 0a828a5a Martin Sebela
454
  $.ajax({
455
    type: 'GET',
456
    url: availableDatesSource,
457 8feb1753 ballakt
    success: function (result) {
458
      availableDates = String(result).split(',')
459 0a828a5a Martin Sebela
    }
460 a7e04778 Martin Sebela
  }).then(function () {
461
    $('#date').datepicker({
462
      format: 'yyyy-mm-dd',
463
      language: 'cs',
464 8feb1753 ballakt
      beforeShowDay: function (date) {
465 a7e04778 Martin Sebela
        if (availableDates.indexOf(formatDate(date)) < 0) {
466 8feb1753 ballakt
          return { enabled: false, tooltip: 'Žádná data' }
467
        } else {
468
          return { enabled: true }
469 a7e04778 Martin Sebela
        }
470
      },
471
      autoclose: true
472 8feb1753 ballakt
    })
473
  })
474
}
475 dd652e61 Martin Sebela
476 81980e82 ballakt
function initLocationsMenu () {
477
  var locationsWrapper = '.locations'
478
  var locationsDisplayClass = 'show'
479 dd652e61 Martin Sebela
480 81980e82 ballakt
  if ($(window).width() <= 480) {
481
    $(locationsWrapper).removeClass(locationsDisplayClass)
482
  } else {
483
    $(locationsWrapper).addClass(locationsDisplayClass)
484 dd652e61 Martin Sebela
  }
485 4e003182 Martin Sebela
}
486
487 81980e82 ballakt
function openDatepicker () {
488 4e003182 Martin Sebela
  if ($(window).width() <= 990) {
489 81980e82 ballakt
    $('.navbar-collapse').collapse()
490 4e003182 Martin Sebela
  }
491
492
  $('#date').datepicker('show')
493 81980e82 ballakt
}
494 2f227a6c ballakt
function onDocumentReady () {
495
  $('#dataset-dropdown').on('click', function (e) {
496
    e.stopPropagation()
497
  })
498
  datasetSelected = []
499
  $('#dataset-dropdown .dropdown-item').each(function () {
500
    const input = $(this).find('input')
501
    const inputVal = input[0].value
502
    if (input[0].checked) {
503
      datasetSelected.push(inputVal)
504
    }
505
  })
506
  $('#submit-btn').prop('name', '')
507
}