Projekt

Obecné

Profil

Stáhnout (16.7 KB) Statistiky
| Větev: | Revize:
1
/* global L */
2
/* global $ */
3

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

    
8
var startX = 49.7248
9
var startY = 13.3521
10
var startZoom = 17
11

    
12
var dataSourceRoute
13
let positionsSourceRoute
14
var currentTime
15

    
16
var timer
17
var isAnimationRunning = false
18
var data = []
19

    
20
var info = []
21
let currentInfo = 0
22

    
23
const datasetDictNameDisplayName = {}
24
var datasetSelected = []
25

    
26
const globalMarkersHolder = {}
27
// all marker from which popup was removed
28
// contains: {key:[L.circle,L.pupup]}
29
// key: x and y, x + '' + y string
30
let globalMarkersChanged = {}
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

    
51
const genPopUpControlButtons = (currentPage, numPages, onNextClick, onPreviousClick) => ({
52
  previousButton: '<button id="previous-info-btn" class="circle-button" onclick="previousInfo()"></button>',
53
  nextButton: '<button id="next-info-btn" onclick="nextInfo()" class="circle-button next"></button>',
54
  posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>`
55
})
56
const genPopUpControls = (controls) => {
57
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
58
}
59
const genMultipleDatasetsPopUp = (sum, currentPos, maxPos, datasetName) => {
60
  const header = `<strong id="dataset-info">${datasetName}</strong>`
61
  const digitInfo = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
62
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
63
  return `
64
  ${header}
65
  ${digitInfo}
66
  ${genPopUpControls([previousButton, posInfo, nextButton])}
67
  `
68
}
69
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
70
  autoPan: false,
71
  className: className
72
}).setLatLng([lat / num, lng / num])
73

    
74
const genPopUp = (datasetName, place, count, sum, currentPos, maxPos) => {
75
  const popupHeader = `
76
    <strong>${datasetName}</strong>
77
    <div id="place-info">${place}</div>`
78
  const popupData = `
79
    <div id="number-info">
80
      <span id="digit-info">${count}</span>
81
      <span id="total-info">${(sum && (sum != count)) ? '/' + sum : ''}</span>
82
    </div>`
83
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
84

    
85
  return `
86
  ${popupHeader}
87
  ${popupData}
88
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
89
  `
90
}
91
const onCheckboxClicked = async (checkbox) => {
92
  if ($(checkbox).prop('checked')) {
93
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
94
    changeUrl()
95
  } else {
96
    loadCheckboxDatasetNameData()
97
    data.forEach((item, index) => {
98
      Object.keys(item).forEach((datasetName) => {
99
        if (datasetName === $(checkbox).val()) {
100
          delete data[index][datasetName]
101
        }
102
      })
103
      drawHeatmap(data[currentTime])
104
    })
105
    changeUrl()
106
  }
107
}
108
const debounce = (func, delay) => {
109
  let inDebounce
110
  return function () {
111
    const context = this
112
    const args = arguments
113
    clearTimeout(inDebounce)
114
    inDebounce = setTimeout(() => func.apply(context, args), delay)
115
  }
116
}
117

    
118
const onValueChangeRegister = () => {
119
  $('#date').change(function () {
120
    data = []
121
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
122
    const date = new Date($(this).val())
123
    $('#player-date').html(`${date.getDate()}. ${date.getMonth() + 1}. ${date.getFullYear()}`)
124
    changeUrl()
125
  })
126

    
127
  $('#dataset-dropdown-time input[type="radio"]').each(function () {
128
    $(this).change(function () {
129
      currentTime = $(this).val()
130
      updateHeaderControls()
131
      setTimeline()
132
      drawHeatmap(data[currentTime])
133
      changeUrl()
134
    })
135
  })
136

    
137
  $('input[type=checkbox]').each(function () {
138
    $(this).change(
139
      debounce(() => onCheckboxClicked(this), 1000)
140
    )
141
  })
142
}
143

    
144
/**
145
 * Initialize leaflet map on start position which can be default or set based on user action
146
 */
147
// eslint-disable-next-line no-unused-vars
148
function initMap () {
149
  startX = localStorage.getItem('lat') || startX
150
  startY = localStorage.getItem('lng') || startY
151
  startZoom = localStorage.getItem('zoom') || startZoom
152

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

    
155
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
156
    attribution: '',
157
    maxZoom: 19
158
  }).addTo(mymap)
159

    
160
  mymap.on('click', showInfo)
161
}
162
const getInfoLength = () => {
163
  const infoKeys = Object.keys(info)
164
  if (infoKeys.length === 1) {
165
    // return number of records in one dataset (one dataset in area)
166
    return info[infoKeys[0]].items.length
167
  }
168
  // return number of datasets (agregation of all datasets in area)
169
  return infoKeys.length
170
}
171
const getElFromObjectInfo = (position) => {
172
  const keys = Object.keys(info)
173
  return info[keys[position]]
174
}
175
const hasInfoMultipleDatasets = () => {
176
  return Object.keys(info).length > 1
177
}
178
function showInfo (e) {
179
  info = []
180
  currentInfo = 0
181

    
182
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
183
  // Todo change to variable - it is used in heatmap init
184
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
185
  const radius = 25 * stile / 256
186

    
187
  let i = 0
188
  let lat = 0
189
  let lng = 0
190

    
191
  let total = 0
192

    
193
  const datasetsInRadius = {}
194

    
195
  Object.keys(data[currentTime]).forEach((key) => {
196
    const namedData = data[currentTime][key]
197
    namedData.items.forEach(element => {
198
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
199
        lat += element.x
200
        lng += element.y
201
        info[i] = { place: element.place, number: element.number, datasetName: key }
202
        total += parseInt(element.number)
203
        i++
204
        datasetsInRadius[key] = true
205
      }
206
    })
207
  })
208

    
209
  // Process info for more then one dataset
210

    
211
  info = info.reduce((acc, item) => {
212
    if (!acc[item.datasetName]) {
213
      acc[item.datasetName] = {
214
        items: [],
215
        number: 0,
216
        datasetName: item.datasetName
217
      }
218
    }
219
    acc[item.datasetName].items.push(item)
220
    acc[item.datasetName].number += Number(item.number)
221
    return acc
222
  }, {})
223

    
224
  // There is one dataset
225

    
226
  const numDatasets = Object.keys(datasetsInRadius).length
227

    
228
  if (!numDatasets) {
229
    return
230
  }
231

    
232
  if (numDatasets === 1) {
233
    const infoDict = getElFromObjectInfo(0)
234
    const info_ = infoDict.items
235
    const { place, number } = info_[currentInfo]
236
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
237
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
238
      .openOn(mymap)
239
    if (info_.length === 1) {
240
      $('#previous-info-btn').prop('disabled', true)
241
      $('#next-info-btn').prop('disabled', true)
242
      $('.popup-controls').hide()
243
    }
244
  } else {
245
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
246
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
247
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
248
      .openOn(mymap)
249
  }
250
}
251

    
252
// eslint-disable-next-line no-unused-vars
253
function previousInfo () {
254
  const infoLength = getInfoLength()
255
  const previousCurrentInfo = currentInfo
256
  currentInfo = (currentInfo + infoLength - 1) % infoLength
257
  displayInfoText(previousCurrentInfo)
258
}
259

    
260
// eslint-disable-next-line no-unused-vars
261
function nextInfo () {
262
  const infoLength = getInfoLength()
263
  const previousCurrentInfo = currentInfo
264
  currentInfo = (currentInfo + 1) % infoLength
265
  displayInfoText(previousCurrentInfo)
266
}
267
function displayInfoText (previousInfoNum) {
268
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
269
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
270
  const infoLength = getInfoLength()
271
  const datasetInfo = $('#dataset-info')
272
  if (datasetInfo) {
273
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
274
  }
275
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
276
  $('#digit-info').html(info_.number)
277
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
278
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
279
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
280
}
281

    
282
// eslint-disable-next-line no-unused-vars
283
function setMapView (latitude, longitude, zoom) {
284
  localStorage.setItem('lat', latitude)
285
  localStorage.setItem('lng', longitude)
286
  localStorage.setItem('zoom', zoom)
287
  mymap.setView([latitude, longitude], zoom)
288
}
289

    
290
/**
291
 * Change animation start from playing to stopped or the other way round
292
 */
293
// eslint-disable-next-line no-unused-vars
294
function changeAnimationState () {
295
  isAnimationRunning = !isAnimationRunning
296
  if (isAnimationRunning) {
297
    $('#play-pause').attr('class', 'pause')
298
    timer = setInterval(
299
      function () {
300
        next()
301
      },
302
      800
303
    )
304
  } else {
305
    clearTimeout(timer)
306
    $('#play-pause').attr('class', 'play')
307
  }
308
}
309

    
310
// eslint-disable-next-line no-unused-vars
311
function previous () {
312
  currentTime = (currentTime + 23) % 24
313
  drawHeatmap(data[currentTime])
314
  setTimeline()
315
  mymap.closePopup()
316
  updateHeaderControls()
317
  changeUrl()
318
}
319

    
320
function next () {
321
  currentTime = (currentTime + 1) % 24
322
  drawHeatmap(data[currentTime])
323
  setTimeline()
324
  mymap.closePopup()
325
  updateHeaderControls()
326
  changeUrl()
327
}
328
const typeUrlReducer = (accumulator, currentValue) => accumulator + currentValue
329
/**
330
 * Change browser url based on animation step
331
 */
332
function changeUrl () {
333
  window.history.pushState(
334
    '',
335
    document.title,
336
    window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
337
  )
338
}
339

    
340
function updateHeaderControls () {
341
  $(`#time_${currentTime}`).prop('checked', true)
342
  $('#dropdownMenuButtonTime').html((currentTime < 10 ? '0' : '') + `${currentTime}:00`)
343
}
344

    
345
function setTimeline () {
346
  $('#timeline').text(currentTime + ':00')
347
  $('#timeline').attr('class', 'time hour-' + currentTime)
348
}
349

    
350
/**
351
 * Load and display heatmap layer for current data
352
 * @param {string} opendataRoute route to dataset source
353
 * @param {string} positionsRoute  route to dataset postitions source
354
 */
355
// eslint-disable-next-line no-unused-vars
356
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
357
  loadCheckboxDatasetNameData()
358
  dataSourceRoute = opendataRoute
359
  positionsSourceRoute = positionsRoute
360
  const dataSourceMarks = {}
361
  const allPromises = []
362
  const date = $('#date').val()
363
  currentTime = parseInt($('#dataset-dropdown-time input[type="radio"]:checked').val())
364

    
365
  setTimeline()
366
  data[currentTime] = {}
367
  const dataSelectedHandler = async (datasetName) => {
368
    const marks = await fetchDataSourceMarks(positionsRoute, datasetName)
369
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, date, currentTime)
370
    dataSourceMarks[datasetName] = marks
371
    data[currentTime][datasetName] = datasetData
372
  }
373
  await datasetSelected.forEach((datasetName) => {
374
    allPromises.push(dataSelectedHandler(datasetName))
375
  })
376
  Promise.all(allPromises).then(
377
    () => {
378
      drawDataSourceMarks(dataSourceMarks)
379
      drawHeatmap(data[currentTime])
380
      preload(currentTime, 1, date)
381
      preload(currentTime, -1, date)
382
    }
383
  )
384
}
385

    
386
function drawDataSourceMarks (data) {
387
  if (marksLayer != null) {
388
    mymap.removeLayer(marksLayer)
389
  }
390
  marksLayer = L.layerGroup()
391
  Object.keys(data).forEach((key_) => {
392
    for (var key in data[key_]) {
393
      const { x, y, name } = data[key_][key]
394
      const pop =
395
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
396
            .setContent(genPopUp(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
397
      const newCircle =
398
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
399
          .bindPopup(pop)
400
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
401
      marksLayer.addLayer(
402
        newCircle
403
      )
404
    }
405
  })
406

    
407
  marksLayer.setZIndex(-1).addTo(mymap)
408
}
409

    
410
async function preload (time, change, date) {
411
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
412
    if (!data[nTime]) {
413
      data[nTime] = {}
414
    }
415
    datasetSelected.forEach(async (datasetName) => {
416
      if (!data[nTime][datasetName]) {
417
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
418
      }
419
    })
420
  }
421
}
422

    
423
function drawHeatmap (dataRaw) {
424
  // Todo still switched
425
  const dataDict = dataRaw
426
  const mergedPoints = []
427
  let max = 0
428
  if (Object.keys(globalMarkersChanged).length) {
429
    Object.keys(globalMarkersChanged).forEach(function (key) {
430
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
431
    })
432
    globalMarkersChanged = {}
433
  }
434
  Object.keys(dataDict).forEach((key) => {
435
    const data = dataDict[key]
436
    max = Math.max(max, data.max)
437
    if (data != null) {
438
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
439
      const points = data.items.map((point) => {
440
        const { x, y, number } = point
441
        const key = x + '' + y
442
        const holder = globalMarkersHolder[key]
443
        if (!globalMarkersChanged[key] && number) {
444
        // There is data for this marker => unbind popup with zero value
445
          holder[0] = holder[0].unbindPopup()
446
          globalMarkersChanged[key] = holder
447
        }
448
        return [x, y, number]
449
      })
450
      mergedPoints.push(...points)
451
    } else {
452
      if (heatmapLayer != null) {
453
        mymap.removeLayer(heatmapLayer)
454
      }
455
    }
456
  })
457
  if (heatmapLayer != null) {
458
    mymap.removeLayer(heatmapLayer)
459
  }
460
  if (mergedPoints.length) {
461
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
462
  }
463
}
464

    
465
/**
466
 * Checks dataset availibility
467
 * @param {string} route authority for datasets availibility checks
468
 */
469
// eslint-disable-next-line no-unused-vars
470
function checkDataSetsAvailability (route) {
471
  $.ajax({
472
    type: 'POST',
473
    // Todo it might be good idea to change db collections format
474
    url: route + '/' + $('#date').val(),
475
    success: function (result) {
476
      updateAvailableDataSets(result)
477
    }
478
  })
479
}
480

    
481
function updateAvailableDataSets (available) {
482
  let leastOneOptionEnabled = false
483
  // datasetSelected = []
484
  $('#dataset-dropdown .dropdown-item').each(function () {
485
    const input = $(this).find('input')
486
    const inputVal = input[0].value
487
    if (!(inputVal in available)) {
488
      $(this).addClass('disabled')
489
      $(input).prop('checked', false)
490
    } else {
491
      leastOneOptionEnabled = true
492
      $(this).removeClass('disabled')
493
    }
494
  })
495

    
496
  $('#submit-btn').prop('disabled', !leastOneOptionEnabled)
497
}
498

    
499
function formatDate (date) {
500
  var day = String(date.getDate())
501
  var month = String(date.getMonth() + 1)
502

    
503
  if (day.length === 1) {
504
    day = '0' + day
505
  }
506

    
507
  if (month.length === 1) {
508
    month = '0' + month
509
  }
510

    
511
  return date.getFullYear() + '-' + month + '-' + day
512
}
513

    
514
// eslint-disable-next-line no-unused-vars
515
function initDatepicker (availableDatesSource) {
516
  var availableDates = ''
517

    
518
  $.ajax({
519
    type: 'GET',
520
    url: availableDatesSource,
521
    success: function (result) {
522
      availableDates = String(result).split(',')
523
    }
524
  }).then(function () {
525
    $('#date').datepicker({
526
      format: 'yyyy-mm-dd',
527
      language: 'cs',
528
      beforeShowDay: function (date) {
529
        if (availableDates.indexOf(formatDate(date)) < 0) {
530
          return { enabled: false, tooltip: 'Žádná data' }
531
        } else {
532
          return { enabled: true }
533
        }
534
      },
535
      autoclose: true
536
    })
537
  })
538
}
539

    
540
function initLocationsMenu () {
541
  var locationsWrapper = '.locations'
542
  var locationsDisplayClass = 'show'
543

    
544
  if ($(window).width() <= 480) {
545
    $(locationsWrapper).removeClass(locationsDisplayClass)
546
  } else {
547
    $(locationsWrapper).addClass(locationsDisplayClass)
548
  }
549
}
550

    
551
function openDatepicker () {
552
  if ($(window).width() <= 990) {
553
    $('.navbar-collapse').collapse()
554
  }
555

    
556
  $('#date').datepicker('show')
557
}
558
function onDocumentReady () {
559
  $('#dataset-dropdown').on('click', function (e) {
560
    e.stopPropagation()
561
  })
562

    
563
  $('#submit-btn').prop('name', '')
564
  onValueChangeRegister()
565
}
566
const loadCheckboxDatasetNameData = () => {
567
  datasetSelected = []
568
  $('#dataset-dropdown .dropdown-item').each(function () {
569
    const input = $(this).find('input')
570
    const inputVal = input[0].value
571
    if (input[0].checked) {
572
      datasetSelected.push(inputVal)
573
    }
574
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
575
  })
576
}
(2-2/2)