Projekt

Obecné

Profil

Stáhnout (16.9 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

    
33
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
34
  const headers = new Headers()
35
  const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, {
36
    method: 'GET',
37
    headers: headers
38
  })
39
  const beforeJson = await fetch(myRequest)
40
  return beforeJson.json()
41
}
42

    
43

    
44
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
45
  const headers = new Headers()
46
  const myRequest = new Request(positionRoute + '/' + datasetName, {
47
    method: 'GET',
48
    headers: headers
49
  })
50
  const beforeJson = await fetch(myRequest)
51
  return beforeJson.json()
52
}
53

    
54

    
55
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" class="circle-button next" onclick="nextInfo()"></button>',
58
  posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>`
59
})
60

    
61

    
62
const genPopUpControls = (controls) => {
63
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
64
}
65

    
66

    
67
const genMultipleDatasetsPopUp = (sum, currentPos, maxPos, datasetName) => {
68
  const popupHeader = `<strong id="dataset-info">${datasetName}</strong>`
69
  const popupData = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
70
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
71

    
72
  return `
73
  ${popupHeader}
74
  ${popupData}
75
  ${genPopUpControls([previousButton, posInfo, nextButton])}
76
  `
77
}
78

    
79

    
80
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
81
  autoPan: false,
82
  className: className
83
}).setLatLng([lat / num, lng / num])
84

    
85

    
86
const genPopUp = (datasetName, place, count, sum, currentPos, maxPos) => {
87
  const popupHeader = `
88
    <strong>${datasetName}</strong>
89
    <div id="place-info">${place}</div>`
90
  const popupData = `
91
    <div id="number-info">
92
      <span id="digit-info">${count}</span>
93
      <span id="total-info">${(sum && (sum != count)) ? '/' + sum : ''}</span>
94
    </div>`
95
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
96

    
97
  return `
98
  ${popupHeader}
99
  ${popupData}
100
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
101
  `
102
}
103

    
104

    
105
const onCheckboxClicked = async (checkbox) => {
106
  if ($(checkbox).prop('checked')) {
107
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
108
    changeUrl()
109
  }
110
  else {
111
    loadCheckboxDatasetNameData()
112

    
113
    data.forEach((item, index) => {
114
      Object.keys(item).forEach((datasetName) => {
115
        if (datasetName === $(checkbox).val()) {
116
          delete data[index][datasetName]
117
        }
118
      })
119
      drawHeatmap(data[currentTime])
120
    })
121

    
122
    changeUrl()
123
  }
124
}
125

    
126

    
127
const debounce = (func, delay) => {
128
  let inDebounce
129
  return function () {
130
    const context = this
131
    const args = arguments
132
    clearTimeout(inDebounce)
133
    inDebounce = setTimeout(() => func.apply(context, args), delay)
134
  }
135
}
136

    
137

    
138
const onValueChangeRegister = () => {
139
  $('#date').change(function () {
140
    data = []
141
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
142
    const date = new Date($(this).val())
143
    $('#player-date').html(`${date.getDate()}. ${date.getMonth() + 1}. ${date.getFullYear()}`)
144
    changeUrl()
145
  })
146

    
147
  $('#dropdown-time input[type="radio"]').each(function () {
148
    $(this).change(function () {
149
      currentTime = parseInt($(this).val())
150
      updateHeaderControls()
151
      setTimeline()
152
      drawHeatmap(data[currentTime])
153
      changeUrl()
154
    })
155
  })
156

    
157
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
158
    $(this).change(
159
      debounce(() => onCheckboxClicked(this), 1000)
160
    )
161
  })
162
}
163

    
164

    
165
/**
166
 * Initialize leaflet map on start position which can be default or set based on user action
167
 */
168
// eslint-disable-next-line no-unused-vars
169
function initMap () {
170
  startX = localStorage.getItem('lat') || startX
171
  startY = localStorage.getItem('lng') || startY
172
  startZoom = localStorage.getItem('zoom') || startZoom
173

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

    
176
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
177
    attribution: '',
178
    maxZoom: 19
179
  }).addTo(mymap)
180

    
181
  mymap.on('click', showInfo)
182
}
183

    
184

    
185
const getInfoLength = () => {
186
  const infoKeys = Object.keys(info)
187
  if (infoKeys.length === 1) {
188
    // return number of records in one dataset (one dataset in area)
189
    return info[infoKeys[0]].items.length
190
  }
191
  // return number of datasets (agregation of all datasets in area)
192
  return infoKeys.length
193
}
194

    
195

    
196
const getElFromObjectInfo = (position) => {
197
  const keys = Object.keys(info)
198
  return info[keys[position]]
199
}
200

    
201

    
202
const hasInfoMultipleDatasets = () => {
203
  return Object.keys(info).length > 1
204
}
205

    
206

    
207
function showInfo (e) {
208
  info = []
209
  currentInfo = 0
210

    
211
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
212
  // Todo change to variable - it is used in heatmap init
213
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
214
  const radius = 25 * stile / 256
215

    
216
  let i = 0
217
  let lat = 0
218
  let lng = 0
219

    
220
  let total = 0
221

    
222
  const datasetsInRadius = {}
223

    
224
  Object.keys(data[currentTime]).forEach((key) => {
225
    const namedData = data[currentTime][key]
226
    namedData.items.forEach(element => {
227
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
228
        lat += element.x
229
        lng += element.y
230
        info[i] = { place: element.place, number: element.number, datasetName: key }
231
        total += parseInt(element.number)
232
        i++
233
        datasetsInRadius[key] = true
234
      }
235
    })
236
  })
237

    
238
  // Process info for more then one dataset
239

    
240
  info = info.reduce((acc, item) => {
241
    if (!acc[item.datasetName]) {
242
      acc[item.datasetName] = {
243
        items: [],
244
        number: 0,
245
        datasetName: item.datasetName
246
      }
247
    }
248

    
249
    acc[item.datasetName].items.push(item)
250
    acc[item.datasetName].number += Number(item.number)
251
    return acc
252
  }, {})
253

    
254
  // There is one dataset
255

    
256
  const numDatasets = Object.keys(datasetsInRadius).length
257

    
258
  if (!numDatasets) {
259
    return
260
  }
261

    
262
  if (numDatasets === 1) {
263
    const infoDict = getElFromObjectInfo(0)
264
    const info_ = infoDict.items
265
    const { place, number } = info_[currentInfo]
266
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
267
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
268
      .openOn(mymap)
269

    
270
    if (info_.length === 1) {
271
      $('#previous-info-btn').prop('disabled', true)
272
      $('#next-info-btn').prop('disabled', true)
273
      $('.popup-controls').hide()
274
    }
275
  }
276
  else {
277
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
278

    
279
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
280
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
281
      .openOn(mymap)
282
  }
283
}
284

    
285

    
286
// eslint-disable-next-line no-unused-vars
287
function previousInfo () {
288
  const infoLength = getInfoLength()
289
  const previousCurrentInfo = currentInfo
290

    
291
  currentInfo = (currentInfo + infoLength - 1) % infoLength
292
  displayInfoText(previousCurrentInfo)
293
}
294

    
295

    
296
// eslint-disable-next-line no-unused-vars
297
function nextInfo () {
298
  const infoLength = getInfoLength()
299
  const previousCurrentInfo = currentInfo
300

    
301
  currentInfo = (currentInfo + 1) % infoLength
302
  displayInfoText(previousCurrentInfo)
303
}
304

    
305

    
306
function displayInfoText (previousInfoNum) {
307
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
308
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
309
  const infoLength = getInfoLength()
310
  const datasetInfo = $('#dataset-info')
311

    
312
  if (datasetInfo) {
313
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
314
  }
315
  
316
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
317
  $('#digit-info').html(info_.number)
318
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
319

    
320
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
321
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
322
}
323

    
324

    
325
// eslint-disable-next-line no-unused-vars
326
function setMapView (latitude, longitude, zoom) {
327
  localStorage.setItem('lat', latitude)
328
  localStorage.setItem('lng', longitude)
329
  localStorage.setItem('zoom', zoom)
330
  mymap.setView([latitude, longitude], zoom)
331
}
332

    
333

    
334
/**
335
 * Change animation start from playing to stopped or the other way round
336
 */
337
// eslint-disable-next-line no-unused-vars
338
function changeAnimationState () {
339
  isAnimationRunning = !isAnimationRunning
340

    
341
  if (isAnimationRunning) {
342
    $('#play-pause').attr('class', 'pause')
343
    timer = setInterval(function() { next() }, 800)
344
  }
345
  else {
346
    clearTimeout(timer)
347
    $('#play-pause').attr('class', 'play')
348
  }
349
}
350

    
351

    
352
// eslint-disable-next-line no-unused-vars
353
function previous () {
354
  currentTime = (currentTime + 23) % 24
355
  drawHeatmap(data[currentTime])
356
  setTimeline()
357
  mymap.closePopup()
358
  updateHeaderControls()
359
  changeUrl()
360
}
361

    
362

    
363
function next () {
364
  currentTime = (currentTime + 1) % 24
365
  drawHeatmap(data[currentTime])
366
  setTimeline()
367
  mymap.closePopup()
368
  updateHeaderControls()
369
  changeUrl()
370
}
371

    
372

    
373
/**
374
 * Change browser url based on animation step.
375
 */
376
function changeUrl () {
377
  window.history.pushState(
378
    '',
379
    document.title,
380
    window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type=' + current, '')}`
381
  )
382
}
383

    
384

    
385
function updateHeaderControls () {
386
  $(`#time_${currentTime}`).prop('checked', true)
387
  $('#dropdownMenuButtonTime').html((currentTime < 10 ? '0' : '') + `${currentTime}:00`)
388
}
389

    
390

    
391
function setTimeline () {
392
  $('#timeline').text(currentTime + ':00')
393
  $('#timeline').attr('class', 'time hour-' + currentTime)
394
}
395

    
396

    
397
function changeHour(hour) {
398
  currentTime = hour
399
  updateHeaderControls()
400
  setTimeline()
401
  drawHeatmap(data[currentTime])
402
  changeUrl()
403
}
404

    
405

    
406
/**
407
 * Load and display heatmap layer for current data
408
 * @param {string} opendataRoute route to dataset source
409
 * @param {string} positionsRoute  route to dataset postitions source
410
 */
411
// eslint-disable-next-line no-unused-vars
412
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
413
  loadCheckboxDatasetNameData()
414

    
415
  dataSourceRoute = opendataRoute
416
  positionsSourceRoute = positionsRoute
417
  const dataSourceMarks = {}
418
  const allPromises = []
419
  const date = $('#date').val()
420
  currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val())
421

    
422
  setTimeline()
423
  data[currentTime] = {}
424
  const dataSelectedHandler = async (datasetName) => {
425
    const marks = await fetchDataSourceMarks(positionsRoute, datasetName)
426
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, date, currentTime)
427
    dataSourceMarks[datasetName] = marks
428
    data[currentTime][datasetName] = datasetData
429
  }
430

    
431
  await datasetSelected.forEach((datasetName) => {
432
    allPromises.push(dataSelectedHandler(datasetName))
433
  })
434

    
435
  Promise.all(allPromises).then(
436
    () => {
437
      drawDataSourceMarks(dataSourceMarks)
438
      drawHeatmap(data[currentTime])
439
      preload(currentTime, 1, date)
440
      preload(currentTime, -1, date)
441
    }
442
  )
443
}
444

    
445

    
446
function drawDataSourceMarks (data) {
447
  if (marksLayer != null) {
448
    mymap.removeLayer(marksLayer)
449
  }
450

    
451
  marksLayer = L.layerGroup()
452

    
453
  Object.keys(data).forEach((key_) => {
454
    for (var key in data[key_]) {
455
      const { x, y, name } = data[key_][key]
456
      const pop =
457
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
458
            .setContent(genPopUp(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
459
      const newCircle =
460
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
461
          .bindPopup(pop)
462
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
463
      marksLayer.addLayer(
464
        newCircle
465
      )
466
    }
467
  })
468

    
469
  marksLayer.setZIndex(-1).addTo(mymap)
470
}
471

    
472

    
473
async function preload (time, change, date) {
474
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
475
    if (!data[nTime]) {
476
      data[nTime] = {}
477
    }
478

    
479
    datasetSelected.forEach(async (datasetName) => {
480
      if (!data[nTime][datasetName]) {
481
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
482
      }
483
    })
484
  }
485
}
486

    
487

    
488
function drawHeatmap (dataRaw) {
489
  // Todo still switched
490
  const dataDict = dataRaw
491
  const mergedPoints = []
492
  let max = 0
493
  
494
  if (Object.keys(globalMarkersChanged).length) {
495
    Object.keys(globalMarkersChanged).forEach(function (key) {
496
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
497
    })
498
    globalMarkersChanged = {}
499
  }
500

    
501
  Object.keys(dataDict).forEach((key) => {
502
    const data = dataDict[key]
503
    max = Math.max(max, data.max)
504

    
505
    if (data != null) {
506
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
507
      const points = data.items.map((point) => {
508
        const { x, y, number } = point
509
        const key = x + '' + y
510
        const holder = globalMarkersHolder[key]
511

    
512
        if (!globalMarkersChanged[key] && number) {
513
        // There is data for this marker => unbind popup with zero value
514
          holder[0] = holder[0].unbindPopup()
515
          globalMarkersChanged[key] = holder
516
        }
517

    
518
        return [x, y, number]
519
      })
520
      mergedPoints.push(...points)
521
    }
522
    else {
523
      if (heatmapLayer != null) {
524
        mymap.removeLayer(heatmapLayer)
525
      }
526
    }
527
  })
528

    
529
  if (heatmapLayer != null) {
530
    mymap.removeLayer(heatmapLayer)
531
  }
532

    
533
  if (mergedPoints.length) {
534
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
535
  }
536
}
537

    
538

    
539
/**
540
 * Checks dataset availibility
541
 * @param {string} route authority for datasets availibility checks
542
 */
543
// eslint-disable-next-line no-unused-vars
544
function checkDataSetsAvailability (route) {
545
  $.ajax({
546
    type: 'POST',
547
    // Todo it might be good idea to change db collections format
548
    url: route + '/' + $('#date').val(),
549
    success: function (result) {
550
      updateAvailableDataSets(result)
551
    }
552
  })
553
}
554

    
555

    
556
function updateAvailableDataSets (available) {
557
  let leastOneOptionEnabled = false
558
  // datasetSelected = []
559

    
560
  $('#dropdown-dataset .dropdown-item').each(function () {
561
    const input = $(this).find('input')
562
    const inputVal = input[0].value
563

    
564
    if (!(inputVal in available)) {
565
      $(this).addClass('disabled')
566
      $(input).prop('checked', false)
567
    }
568
    else {
569
      leastOneOptionEnabled = true
570
      $(this).removeClass('disabled')
571
    }
572
  })
573

    
574
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
575
}
576

    
577

    
578
function formatDate (date) {
579
  var day = String(date.getDate())
580
  var month = String(date.getMonth() + 1)
581

    
582
  if (day.length === 1) {
583
    day = '0' + day
584
  }
585

    
586
  if (month.length === 1) {
587
    month = '0' + month
588
  }
589

    
590
  return date.getFullYear() + '-' + month + '-' + day
591
}
592

    
593

    
594
// eslint-disable-next-line no-unused-vars
595
function initDatepicker (availableDatesSource) {
596
  var availableDates = ''
597

    
598
  $.ajax({
599
    type: 'GET',
600
    url: availableDatesSource,
601
    success: function (result) {
602
      availableDates = String(result).split(',')
603
    }
604
  }).then(function () {
605
    $('#date').datepicker({
606
      format: 'yyyy-mm-dd',
607
      language: 'cs',
608
      beforeShowDay: function (date) {
609
        if (availableDates.indexOf(formatDate(date)) < 0) {
610
          return { enabled: false, tooltip: 'Žádná data' }
611
        }
612
        else {
613
          return { enabled: true }
614
        }
615
      },
616
      autoclose: true
617
    })
618
  })
619
}
620

    
621

    
622
function initLocationsMenu () {
623
  var locationsWrapper = '.locations'
624
  var locationsDisplayClass = 'show'
625

    
626
  if ($(window).width() <= 480) {
627
    $(locationsWrapper).removeClass(locationsDisplayClass)
628
  }
629
  else {
630
    $(locationsWrapper).addClass(locationsDisplayClass)
631
  }
632
}
633

    
634

    
635
function openDatepicker () {
636
  if ($(window).width() <= 990) {
637
    $('.navbar-collapse').collapse()
638
  }
639

    
640
  $('#date').datepicker('show')
641
}
642

    
643

    
644
function onDocumentReady () {
645
  $('#dropdown-dataset').on('click', function (e) {
646
    e.stopPropagation()
647
  })
648

    
649
  $('#btn-update-heatmap').prop('name', '')
650
  onValueChangeRegister()
651
}
652

    
653

    
654
const loadCheckboxDatasetNameData = () => {
655
  datasetSelected = []
656
  $('#dropdown-dataset .dropdown-item').each(function () {
657
    const input = $(this).find('input')
658
    const inputVal = input[0].value
659

    
660
    if (input[0].checked) {
661
      datasetSelected.push(inputVal)
662
    }
663

    
664
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
665
  })
666
}
(2-2/2)