Projekt

Obecné

Profil

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

    
15
let currentTime
16

    
17
let currentDate
18

    
19
var timer
20
var isAnimationRunning = false
21
var data = []
22

    
23
var info = []
24
let currentInfo = 0
25

    
26
const datasetDictNameDisplayName = {}
27
var datasetSelected = []
28

    
29
// data only for one day
30
let lockedDay = false
31

    
32
let loading = 0
33

    
34
// marks for all datasets
35
const dataSourceMarks = {}
36

    
37
const globalMarkersHolder = {}
38
// all marker from which popup was removed
39
// contains: {key:[L.circle,L.pupup]}
40
// key: x and y, x + '' + y string
41
let globalMarkersChanged = {}
42

    
43
const loadingY = () => {
44
  loading++
45
}
46
const loadingN = () => {
47
  loading--
48
}
49

    
50
const changeCurrentTime = (time = null) => {
51
  if (time !== null) {
52
    currentTime = time
53
  } else {
54
    $('#dropdown-time input[type="radio"]:checked').each(function () {
55
      currentTime = parseInt($(this).val())
56
    })
57
  }
58
}
59

    
60
const changeCurrentDate = (date = null) => {
61
  if (date) {
62
    currentDate = new Date(date)
63
  } else {
64
    currentDate = new Date($('#date').val())
65
  }
66
  $('#player-date span').html(`${currentDate.getDate()}. ${currentDate.getMonth() + 1}. ${currentDate.getFullYear()}`)
67
  $('#date').val(currentDateToString())
68
  data = []
69
}
70
const currentDayToString = () => {
71
  const day = currentDate.getDate()
72
  return day > 9 ? `${day}` : `0${day}`
73
}
74
const currentMonthToString = () => {
75
  const month = currentDate.getMonth() + 1
76
  return month > 9 ? `${month}` : `0${month}`
77
}
78
const currentDateToString = () => `${currentDate.getFullYear()}-${currentMonthToString()}-${currentDayToString()}`
79
const addDayToCurrentDate = (day) => {
80
  currentDate.setDate(currentDate.getDate() + day)
81
  changeCurrentDate(currentDate)
82
}
83
const toggleDayLock = () => {
84
  lockedDay = !lockedDay
85
  $('#player-date').toggleClass('lock')
86
}
87

    
88
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
89
  const headers = new Headers()
90
  const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, {
91
    method: 'GET',
92
    headers: headers
93
  })
94
  const beforeJson = await fetch(myRequest)
95
  return beforeJson.json()
96
}
97

    
98
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
99
  const headers = new Headers()
100
  const myRequest = new Request(positionRoute + '/' + datasetName, {
101
    method: 'GET',
102
    headers: headers
103
  })
104
  const beforeJson = await fetch(myRequest)
105
  return beforeJson.json()
106
}
107

    
108
const genPopUpControlButtons = (currentPage, numPages, onNextClick, onPreviousClick) => ({
109
  previousButton: '<button id="previous-info-btn" class="circle-button" onclick="previousInfo()"></button>',
110
  nextButton: '<button id="next-info-btn" class="circle-button next" onclick="nextInfo()"></button>',
111
  posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>`
112
})
113

    
114
const genPopUpControls = (controls) => {
115
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
116
}
117

    
118
const genMultipleDatasetsPopUp = (sum, currentPos, maxPos, datasetName) => {
119
  const popupHeader = `<strong id="dataset-info">${datasetName}</strong>`
120
  const popupData = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
121
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
122

    
123
  return `
124
  ${popupHeader}
125
  ${popupData}
126
  ${genPopUpControls([previousButton, posInfo, nextButton])}
127
  `
128
}
129

    
130
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
131
  autoPan: false,
132
  className: className
133
}).setLatLng([lat / num, lng / num])
134

    
135
const genPopUp = (datasetName, place, count, sum, currentPos, maxPos) => {
136
  const popupHeader = `
137
    <strong>${datasetName}</strong>
138
    <div id="place-info">${place}</div>`
139
  const popupData = `
140
    <div id="number-info">
141
      <span id="digit-info">${count}</span>
142
      <span id="total-info">${(sum && (sum != count)) ? '/' + sum : ''}</span>
143
    </div>`
144
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
145

    
146
  return `
147
  ${popupHeader}
148
  ${popupData}
149
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
150
  `
151
}
152

    
153
const onCheckboxClicked = async (checkbox) => {
154
  if ($(checkbox).prop('checked')) {
155
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
156
    changeUrl()
157
  } else {
158
    loadCheckboxDatasetNameData()
159

    
160
    data.forEach((item, index) => {
161
      Object.keys(item).forEach((datasetName) => {
162
        if (datasetName === $(checkbox).val()) {
163
          delete data[index][datasetName]
164
        }
165
      })
166
      drawHeatmap(data[currentTime])
167
    })
168

    
169
    changeUrl()
170
  }
171
}
172

    
173
const debounce = (func, delay) => {
174
  let inDebounce
175
  return function () {
176
    const context = this
177
    const args = arguments
178
    clearTimeout(inDebounce)
179
    inDebounce = setTimeout(() => func.apply(context, args), delay)
180
  }
181
}
182

    
183
const onValueChangeRegister = () => {
184
  $('#date').change(function () {
185
    changeCurrentDate($(this).val())
186
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
187
    changeUrl()
188
  })
189

    
190
  $('#dropdown-time input[type="radio"]').each(function () {
191
    $(this).change(function () {
192
      changeHour(parseInt($(this).val()))
193
      drawHeatmap(data[currentTime])
194
    })
195
  })
196

    
197
  $('#dropdown-dataset input[type="checkbox"]').each(function () {
198
    $(this).change(
199
      debounce(() => onCheckboxClicked(this), 1000)
200
    )
201
  })
202
}
203

    
204
/**
205
 * Initialize leaflet map on start position which can be default or set based on user action
206
 */
207
// eslint-disable-next-line no-unused-vars
208
function initMap () {
209
  startX = localStorage.getItem('lat') || startX
210
  startY = localStorage.getItem('lng') || startY
211
  startZoom = localStorage.getItem('zoom') || startZoom
212

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

    
215
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
216
    attribution: '',
217
    maxZoom: 19
218
  }).addTo(mymap)
219

    
220
  mymap.on('click', showInfo)
221
}
222

    
223
const getInfoLength = () => {
224
  const infoKeys = Object.keys(info)
225
  if (infoKeys.length === 1) {
226
    // return number of records in one dataset (one dataset in area)
227
    return info[infoKeys[0]].items.length
228
  }
229
  // return number of datasets (agregation of all datasets in area)
230
  return infoKeys.length
231
}
232

    
233
const getElFromObjectInfo = (position) => {
234
  const keys = Object.keys(info)
235
  return info[keys[position]]
236
}
237

    
238
const hasInfoMultipleDatasets = () => {
239
  return Object.keys(info).length > 1
240
}
241

    
242
function showInfo (e) {
243
  info = []
244
  currentInfo = 0
245

    
246
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
247
  // Todo change to variable - it is used in heatmap init
248
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
249
  const radius = 25 * stile / 256
250

    
251
  let i = 0
252
  let lat = 0
253
  let lng = 0
254

    
255
  let total = 0
256

    
257
  const datasetsInRadius = {}
258

    
259
  Object.keys(data[currentTime]).forEach((key) => {
260
    const namedData = data[currentTime][key]
261
    namedData.items.forEach(element => {
262
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
263
        lat += element.x
264
        lng += element.y
265
        info[i] = { place: element.place, number: element.number, datasetName: key }
266
        total += parseInt(element.number)
267
        i++
268
        datasetsInRadius[key] = true
269
      }
270
    })
271
  })
272

    
273
  // Process info for more then one dataset
274

    
275
  info = info.reduce((acc, item) => {
276
    if (!acc[item.datasetName]) {
277
      acc[item.datasetName] = {
278
        items: [],
279
        number: 0,
280
        datasetName: item.datasetName
281
      }
282
    }
283

    
284
    acc[item.datasetName].items.push(item)
285
    acc[item.datasetName].number += Number(item.number)
286
    return acc
287
  }, {})
288

    
289
  // There is one dataset
290

    
291
  const numDatasets = Object.keys(datasetsInRadius).length
292

    
293
  if (!numDatasets) {
294
    return
295
  }
296

    
297
  if (numDatasets === 1) {
298
    const infoDict = getElFromObjectInfo(0)
299
    const info_ = infoDict.items
300
    const { place, number } = info_[currentInfo]
301
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
302
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
303
      .openOn(mymap)
304

    
305
    if (info_.length === 1) {
306
      $('#previous-info-btn').prop('disabled', true)
307
      $('#next-info-btn').prop('disabled', true)
308
      $('.popup-controls').hide()
309
    }
310
  } else {
311
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
312

    
313
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
314
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
315
      .openOn(mymap)
316
  }
317
}
318

    
319
// eslint-disable-next-line no-unused-vars
320
function previousInfo () {
321
  const infoLength = getInfoLength()
322
  const previousCurrentInfo = currentInfo
323

    
324
  currentInfo = (currentInfo + infoLength - 1) % infoLength
325
  displayInfoText(previousCurrentInfo)
326
}
327

    
328
// eslint-disable-next-line no-unused-vars
329
function nextInfo () {
330
  const infoLength = getInfoLength()
331
  const previousCurrentInfo = currentInfo
332

    
333
  currentInfo = (currentInfo + 1) % infoLength
334
  displayInfoText(previousCurrentInfo)
335
}
336

    
337
function displayInfoText (previousInfoNum) {
338
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
339
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
340
  const infoLength = getInfoLength()
341
  const datasetInfo = $('#dataset-info')
342

    
343
  if (datasetInfo) {
344
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
345
  }
346

    
347
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
348
  $('#digit-info').html(info_.number)
349
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
350

    
351
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
352
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
353
}
354

    
355
// eslint-disable-next-line no-unused-vars
356
function setMapView (latitude, longitude, zoom) {
357
  localStorage.setItem('lat', latitude)
358
  localStorage.setItem('lng', longitude)
359
  localStorage.setItem('zoom', zoom)
360
  mymap.setView([latitude, longitude], zoom)
361
}
362

    
363
/**
364
 * Change animation start from playing to stopped or the other way round
365
 */
366
// eslint-disable-next-line no-unused-vars
367
function changeAnimationState () {
368
  isAnimationRunning = !isAnimationRunning
369

    
370
  if (isAnimationRunning) {
371
    $('#play-pause').attr('class', 'pause')
372
    timer = setInterval(function () { next() }, 800)
373
  } else {
374
    clearTimeout(timer)
375
    $('#play-pause').attr('class', 'play')
376
  }
377
}
378

    
379
// eslint-disable-next-line no-unused-vars
380
async function previous () {
381
  if (loading) {
382
    return
383
  }
384
  currentTime = (currentTime + 23) % 24
385
  changeHour(currentTime)
386
  mymap.closePopup()
387
  if (!lockedDay && (currentTime === 23)) {
388
    addDayToCurrentDate(-1)
389
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
390
  } else {
391
    drawHeatmap(data[currentTime])
392
  }
393
}
394

    
395
async function next () {
396
  if (loading) {
397
    return
398
  }
399
  currentTime = (currentTime + 1) % 24
400
  changeHour(currentTime)
401
  mymap.closePopup()
402
  if (!lockedDay && (currentTime === 0)) {
403
    addDayToCurrentDate(1)
404
    await loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
405
  } else {
406
    drawHeatmap(data[currentTime])
407
  }
408
}
409

    
410
/**
411
 * Change browser url based on animation step.
412
 */
413
function changeUrl () {
414
  window.history.pushState(
415
    '',
416
    document.title,
417
    window.location.origin + window.location.pathname + `?date=${currentDateToString()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type=' + current, '')}`
418
  )
419
}
420

    
421
function updateHeaderControls () {
422
  $(`#time_${currentTime}`).prop('checked', true)
423
  $('#dropdownMenuButtonTime').html((currentTime < 10 ? '0' : '') + `${currentTime}:00`)
424
}
425

    
426
function setTimeline () {
427
  $('#timeline').text(currentTime + ':00')
428
  $('#timeline').attr('class', 'time hour-' + currentTime)
429
}
430
const onChangeHour = (hour) => {
431
  changeHour(hour)
432
  drawHeatmap(data[currentTime])
433
}
434
const changeHour = (hour) => {
435
  changeCurrentTime(hour)
436
  updateHeaderControls()
437
  setTimeline()
438
  changeUrl()
439
}
440

    
441
/**
442
 * Load and display heatmap layer for current data
443
 * @param {string} opendataRoute route to dataset source
444
 * @param {string} positionsRoute  route to dataset postitions source
445
 */
446
// eslint-disable-next-line no-unused-vars
447
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
448
  loadCheckboxDatasetNameData()
449

    
450
  dataSourceRoute = opendataRoute
451
  positionsSourceRoute = positionsRoute
452
  const allPromises = []
453
  data[currentTime] = {}
454

    
455
  const dataSelectedHandler = async (datasetName) => {
456
    if (!(datasetName in dataSourceMarks)) {
457
      dataSourceMarks[datasetName] = await fetchDataSourceMarks(positionsRoute, datasetName)
458
    }
459
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, currentDateToString(), currentTime)
460
    data[currentTime][datasetName] = datasetData
461
  }
462
  datasetSelected.forEach((datasetName) => {
463
    allPromises.push(dataSelectedHandler(datasetName))
464
  })
465

    
466
  loadingY()
467
  Promise.all(allPromises).then(
468
    () => {
469
      loadingN()
470
      drawDataSourceMarks(dataSourceMarks)
471
      drawHeatmap(data[currentTime])
472
      preload(currentTime, 1, currentDateToString())
473
      preload(currentTime, -1, currentDateToString())
474
    }
475
  )
476
}
477

    
478
function drawDataSourceMarks (data) {
479
  if (marksLayer != null) {
480
    mymap.removeLayer(marksLayer)
481
  }
482

    
483
  marksLayer = L.layerGroup()
484

    
485
  Object.keys(data).forEach((key_) => {
486
    for (var key in data[key_]) {
487
      const { x, y, name } = data[key_][key]
488
      const pop =
489
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
490
            .setContent(genPopUp(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
491
      const newCircle =
492
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
493
          .bindPopup(pop)
494
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
495
      marksLayer.addLayer(
496
        newCircle
497
      )
498
    }
499
  })
500

    
501
  marksLayer.setZIndex(-1).addTo(mymap)
502
}
503

    
504
async function preload (time, change, date) {
505
  loadingY()
506
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
507
    if (!data[nTime]) {
508
      data[nTime] = {}
509
    }
510

    
511
    datasetSelected.forEach(async (datasetName) => {
512
      if (!data[nTime][datasetName]) {
513
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
514
      }
515
    })
516
  }
517
  loadingN()
518
}
519

    
520
function drawHeatmap (dataRaw) {
521
  // Todo still switched
522
  const dataDict = dataRaw
523
  const mergedPoints = []
524
  let max = 0
525

    
526
  if (Object.keys(globalMarkersChanged).length) {
527
    Object.keys(globalMarkersChanged).forEach(function (key) {
528
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
529
    })
530
    globalMarkersChanged = {}
531
  }
532

    
533
  Object.keys(dataDict).forEach((key) => {
534
    const data = dataDict[key]
535
    max = Math.max(max, data.max)
536

    
537
    if (data != null) {
538
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
539
      const points = data.items.map((point) => {
540
        const { x, y, number } = point
541
        const key = x + '' + y
542
        const holder = globalMarkersHolder[key]
543

    
544
        if (!globalMarkersChanged[key] && number) {
545
        // There is data for this marker => unbind popup with zero value
546
          holder[0] = holder[0].unbindPopup()
547
          globalMarkersChanged[key] = holder
548
        }
549

    
550
        return [x, y, number]
551
      })
552
      mergedPoints.push(...points)
553
    } else {
554
      if (heatmapLayer != null) {
555
        mymap.removeLayer(heatmapLayer)
556
      }
557
    }
558
  })
559

    
560
  if (heatmapLayer != null) {
561
    mymap.removeLayer(heatmapLayer)
562
  }
563

    
564
  if (mergedPoints.length) {
565
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
566
  }
567
}
568

    
569
/**
570
 * Checks dataset availibility
571
 * @param {string} route authority for datasets availibility checks
572
 */
573
// eslint-disable-next-line no-unused-vars
574
function checkDataSetsAvailability (route) {
575
  $.ajax({
576
    type: 'POST',
577
    // Todo it might be good idea to change db collections format
578
    url: route + '/' + currentDateToString(),
579
    success: function (result) {
580
      updateAvailableDataSets(result)
581
    }
582
  })
583
}
584

    
585
function updateAvailableDataSets (available) {
586
  let leastOneOptionEnabled = false
587

    
588
  $('#dropdown-dataset .dropdown-item').each(function () {
589
    const input = $(this).find('input')
590
    const inputVal = input[0].value
591

    
592
    if (!(inputVal in available)) {
593
      $(this).addClass('disabled')
594
      $(input).prop('checked', false)
595
    } else {
596
      leastOneOptionEnabled = true
597
      $(this).removeClass('disabled')
598
    }
599
  })
600

    
601
  $('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled)
602
}
603

    
604
function formatDate (date) {
605
  var day = String(date.getDate())
606
  var month = String(date.getMonth() + 1)
607

    
608
  if (day.length === 1) {
609
    day = '0' + day
610
  }
611

    
612
  if (month.length === 1) {
613
    month = '0' + month
614
  }
615

    
616
  return date.getFullYear() + '-' + month + '-' + day
617
}
618

    
619
// eslint-disable-next-line no-unused-vars
620
function initDatepicker (availableDatesSource) {
621
  var availableDates = ''
622

    
623
  $.ajax({
624
    type: 'GET',
625
    url: availableDatesSource,
626
    success: function (result) {
627
      availableDates = String(result).split(',')
628
    }
629
  }).then(function () {
630
    $('#date').datepicker({
631
      format: 'yyyy-mm-dd',
632
      language: 'cs',
633
      beforeShowDay: function (date) {
634
        if (availableDates.indexOf(formatDate(date)) < 0) {
635
          return { enabled: false, tooltip: 'Žádná data' }
636
        } else {
637
          return { enabled: true }
638
        }
639
      },
640
      autoclose: true
641
    })
642
  })
643
}
644

    
645
function initLocationsMenu () {
646
  var locationsWrapper = '.locations'
647
  var locationsDisplayClass = 'show'
648

    
649
  if ($(window).width() <= 480) {
650
    $(locationsWrapper).removeClass(locationsDisplayClass)
651
  } else {
652
    $(locationsWrapper).addClass(locationsDisplayClass)
653
  }
654
}
655

    
656
function onDocumentReady () {
657
  $('#dropdown-dataset').on('click', function (e) {
658
    e.stopPropagation()
659
  })
660

    
661
  $('#btn-update-heatmap').prop('name', '')
662
  changeCurrentTime()
663
  setTimeline()
664
  changeCurrentDate()
665
  onValueChangeRegister()
666
}
667

    
668
const loadCheckboxDatasetNameData = () => {
669
  datasetSelected = []
670
  $('#dropdown-dataset .dropdown-item').each(function () {
671
    const input = $(this).find('input')
672
    const inputVal = input[0].value
673

    
674
    if (input[0].checked) {
675
      datasetSelected.push(inputVal)
676
    }
677

    
678
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
679
  })
680
}
(2-2/2)