Projekt

Obecné

Profil

Stáhnout (16.8 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, number, sum, currentPos, maxPos) => {
75
  const header = `<strong>${datasetName}</strong><div id="place-info">${place}</div>`
76
  const currentNum = `<span id="digit-info">${number}</span>`
77
  // eslint-disable-next-line eqeqeq
78
  const sumNum = `<span id="total-info" style="font-size: large">${(sum && (sum != number)) ? '/' + sum : ''}</span>`
79
  const digitInfo = `<div id="number-info">${currentNum}${sumNum}</div>`
80
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
81
  return `
82
  ${header}
83
  ${digitInfo}
84
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
85
  `
86
}
87
const onCheckboxClicked = async (checkbox) => {
88
  if ($(checkbox).prop('checked')) {
89
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
90
    changeUrl()
91
  } else {
92
    loadCheckboxDatasetNameData()
93
    data.forEach((item, index) => {
94
      Object.keys(item).forEach((datasetName) => {
95
        if (datasetName === $(checkbox).val()) {
96
          delete data[index][datasetName]
97
        }
98
      })
99
      drawHeatmap(data[currentTime])
100
    })
101
    changeUrl()
102
  }
103
}
104
const debounce = (func, delay) => {
105
  let inDebounce
106
  return function () {
107
    const context = this
108
    const args = arguments
109
    clearTimeout(inDebounce)
110
    inDebounce = setTimeout(() => func.apply(context, args), delay)
111
  }
112
}
113
const onValueChangeRegister = () => {
114
  $('#date').change(function () {
115
    data = []
116
    loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute)
117
    console.log('VAL:', $(this).val())
118
    const date = new Date($(this).val())
119
    $('#player-date').html(`${date.getDate()}. ${date.getMonth() + 1}. ${date.getFullYear()}`)
120
  })
121
  $('#dataset-dropdown-time input[type="radio"]').each(function () {
122
    $(this).change(function () {
123
      currentTime = $(this).val()
124
      updateHeaderControls()
125
      setTimeline()
126
      drawHeatmap(data[currentTime])
127
    })
128
  })
129
  $('input[type=checkbox]').each(function () {
130
    $(this).change(
131
      debounce(() => onCheckboxClicked(this), 1000)
132
    )
133
  })
134
}
135
/**
136
 * Initialize leaflet map on start position which can be default or set based on user action
137
 */
138
// eslint-disable-next-line no-unused-vars
139
function initMap () {
140
  startX = localStorage.getItem('lat') || startX
141
  startY = localStorage.getItem('lng') || startY
142
  startZoom = localStorage.getItem('zoom') || startZoom
143

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

    
146
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
147
    attribution: '',
148
    maxZoom: 19
149
  }).addTo(mymap)
150

    
151
  mymap.on('click', showInfo)
152
}
153
const getInfoLength = () => {
154
  const infoKeys = Object.keys(info)
155
  if (infoKeys.length === 1) {
156
    // return number of records in one dataset (one dataset in area)
157
    return info[infoKeys[0]].items.length
158
  }
159
  // return number of datasets (agregation of all datasets in area)
160
  return infoKeys.length
161
}
162
const getElFromObjectInfo = (position) => {
163
  const keys = Object.keys(info)
164
  return info[keys[position]]
165
}
166
const hasInfoMultipleDatasets = () => {
167
  return Object.keys(info).length > 1
168
}
169
function showInfo (e) {
170
  info = []
171
  currentInfo = 0
172

    
173
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
174
  // Todo change to variable - it is used in heatmap init
175
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
176
  const radius = 25 * stile / 256
177

    
178
  let i = 0
179
  let lat = 0
180
  let lng = 0
181

    
182
  let total = 0
183

    
184
  const datasetsInRadius = {}
185

    
186
  Object.keys(data[currentTime]).forEach((key) => {
187
    const namedData = data[currentTime][key]
188
    namedData.items.forEach(element => {
189
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
190
        lat += element.x
191
        lng += element.y
192
        info[i] = { place: element.place, number: element.number, datasetName: key }
193
        total += parseInt(element.number)
194
        i++
195
        datasetsInRadius[key] = true
196
      }
197
    })
198
  })
199

    
200
  // Process info for more then one dataset
201

    
202
  info = info.reduce((acc, item) => {
203
    if (!acc[item.datasetName]) {
204
      acc[item.datasetName] = {
205
        items: [],
206
        number: 0,
207
        datasetName: item.datasetName
208
      }
209
    }
210
    acc[item.datasetName].items.push(item)
211
    acc[item.datasetName].number += Number(item.number)
212
    return acc
213
  }, {})
214

    
215
  // There is one dataset
216

    
217
  const numDatasets = Object.keys(datasetsInRadius).length
218

    
219
  if (!numDatasets) {
220
    return
221
  }
222

    
223
  if (numDatasets === 1) {
224
    const infoDict = getElFromObjectInfo(0)
225
    const info_ = infoDict.items
226
    const { place, number } = info_[currentInfo]
227
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
228
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
229
      .openOn(mymap)
230
    if (info_.length === 1) {
231
      $('#previous-info-btn').prop('disabled', true)
232
      $('#next-info-btn').prop('disabled', true)
233
      $('.popup-controls').hide()
234
    }
235
  } else {
236
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
237
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
238
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
239
      .openOn(mymap)
240
  }
241
}
242

    
243
// eslint-disable-next-line no-unused-vars
244
function previousInfo () {
245
  const infoLength = getInfoLength()
246
  const previousCurrentInfo = currentInfo
247
  currentInfo = (currentInfo + infoLength - 1) % infoLength
248
  displayInfoText(previousCurrentInfo)
249
}
250

    
251
// eslint-disable-next-line no-unused-vars
252
function nextInfo () {
253
  const infoLength = getInfoLength()
254
  const previousCurrentInfo = currentInfo
255
  currentInfo = (currentInfo + 1) % infoLength
256
  displayInfoText(previousCurrentInfo)
257
}
258
function displayInfoText (previousInfoNum) {
259
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
260
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
261
  const infoLength = getInfoLength()
262
  const datasetInfo = $('#dataset-info')
263
  if (datasetInfo) {
264
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
265
  }
266
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
267
  $('#digit-info').html(info_.number)
268
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
269
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
270
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
271
}
272

    
273
// eslint-disable-next-line no-unused-vars
274
function setMapView (latitude, longitude, zoom) {
275
  localStorage.setItem('lat', latitude)
276
  localStorage.setItem('lng', longitude)
277
  localStorage.setItem('zoom', zoom)
278
  mymap.setView([latitude, longitude], zoom)
279
}
280

    
281
/**
282
 * Change animation start from playing to stopped or the other way round
283
 */
284
// eslint-disable-next-line no-unused-vars
285
function changeAnimationState () {
286
  isAnimationRunning = !isAnimationRunning
287
  if (isAnimationRunning) {
288
    $('#play-pause').attr('class', 'pause')
289
    timer = setInterval(
290
      function () {
291
        next()
292
      },
293
      800
294
    )
295
  } else {
296
    clearTimeout(timer)
297
    $('#play-pause').attr('class', 'play')
298
  }
299
}
300

    
301
// eslint-disable-next-line no-unused-vars
302
function previous () {
303
  currentTime = (currentTime + 23) % 24
304
  drawHeatmap(data[currentTime])
305
  setTimeline()
306
  mymap.closePopup()
307
  updateHeaderControls()
308
  changeUrl()
309
}
310

    
311
function next () {
312
  currentTime = (currentTime + 1) % 24
313
  drawHeatmap(data[currentTime])
314
  setTimeline()
315
  mymap.closePopup()
316
  updateHeaderControls()
317
  changeUrl()
318
}
319
const typeUrlReducer = (accumulator, currentValue) => accumulator + currentValue
320
/**
321
 * Change browser url based on animation step
322
 */
323
function changeUrl () {
324
  window.history.pushState(
325
    '',
326
    document.title,
327
    window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
328
  )
329
}
330

    
331
function updateHeaderControls () {
332
  $(`#time_${currentTime}`).prop('checked', true)
333
  $('#dropdownMenuButton-time').html(currentTime >= 10 ? `${currentTime}:00` : `0${currentTime}:00`)
334
}
335

    
336
function setTimeline () {
337
  $('#timeline').text(currentTime + ':00')
338
  $('#timeline').attr('class', 'time hour-' + currentTime)
339
}
340

    
341
/**
342
 * Load and display heatmap layer for current data
343
 * @param {string} opendataRoute route to dataset source
344
 * @param {string} positionsRoute  route to dataset postitions source
345
 */
346
// eslint-disable-next-line no-unused-vars
347
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
348
  loadCheckboxDatasetNameData()
349
  dataSourceRoute = opendataRoute
350
  positionsSourceRoute = positionsRoute
351
  const dataSourceMarks = {}
352
  const allPromises = []
353
  const date = $('#date').val()
354
  currentTime = parseInt($('#dataset-dropdown-time input[type="radio"]:checked').val())
355

    
356
  setTimeline()
357
  data[currentTime] = {}
358
  const dataSelectedHandler = async (datasetName) => {
359
    const marks = await fetchDataSourceMarks(positionsRoute, datasetName)
360
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, date, currentTime)
361
    dataSourceMarks[datasetName] = marks
362
    data[currentTime][datasetName] = datasetData
363
  }
364
  await datasetSelected.forEach((datasetName) => {
365
    allPromises.push(dataSelectedHandler(datasetName))
366
  })
367
  Promise.all(allPromises).then(
368
    () => {
369
      drawDataSourceMarks(dataSourceMarks)
370
      drawHeatmap(data[currentTime])
371
      preload(currentTime, 1, date)
372
      preload(currentTime, -1, date)
373
    }
374
  )
375
}
376

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

    
398
  marksLayer.setZIndex(-1).addTo(mymap)
399
}
400

    
401
async function preload (time, change, date) {
402
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
403
    if (!data[nTime]) {
404
      data[nTime] = {}
405
    }
406
    datasetSelected.forEach(async (datasetName) => {
407
      if (!data[nTime][datasetName]) {
408
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
409
      }
410
    })
411
  }
412
}
413

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

    
456
/**
457
 * Checks dataset availibility
458
 * @param {string} route authority for datasets availibility checks
459
 */
460
// eslint-disable-next-line no-unused-vars
461
function checkDataSetsAvailability (route) {
462
  $.ajax({
463
    type: 'POST',
464
    // Todo it might be good idea to change db collections format
465
    url: route + '/' + $('#date').val(),
466
    success: function (result) {
467
      updateAvailableDataSets(result)
468
    }
469
  })
470
}
471

    
472
function updateAvailableDataSets (available) {
473
  let leastOneOptionEnabled = false
474
  // datasetSelected = []
475
  $('#dataset-dropdown .dropdown-item').each(function () {
476
    const input = $(this).find('input')
477
    const inputVal = input[0].value
478
    if (!(inputVal in available)) {
479
      $(this).addClass('disabled')
480
      $(input).prop('checked', false)
481
    } else {
482
      leastOneOptionEnabled = true
483
      $(this).removeClass('disabled')
484
    }
485
  })
486

    
487
  $('#submit-btn').prop('disabled', !leastOneOptionEnabled)
488
}
489

    
490
function formatDate (date) {
491
  var day = String(date.getDate())
492
  var month = String(date.getMonth() + 1)
493

    
494
  if (day.length === 1) {
495
    day = '0' + day
496
  }
497

    
498
  if (month.length === 1) {
499
    month = '0' + month
500
  }
501

    
502
  return date.getFullYear() + '-' + month + '-' + day
503
}
504

    
505
// eslint-disable-next-line no-unused-vars
506
function initDatepicker (availableDatesSource) {
507
  var availableDates = ''
508

    
509
  $.ajax({
510
    type: 'GET',
511
    url: availableDatesSource,
512
    success: function (result) {
513
      availableDates = String(result).split(',')
514
    }
515
  }).then(function () {
516
    $('#date').datepicker({
517
      format: 'yyyy-mm-dd',
518
      language: 'cs',
519
      beforeShowDay: function (date) {
520
        if (availableDates.indexOf(formatDate(date)) < 0) {
521
          return { enabled: false, tooltip: 'Žádná data' }
522
        } else {
523
          return { enabled: true }
524
        }
525
      },
526
      autoclose: true
527
    })
528
  })
529
}
530

    
531
function initLocationsMenu () {
532
  var locationsWrapper = '.locations'
533
  var locationsDisplayClass = 'show'
534

    
535
  if ($(window).width() <= 480) {
536
    $(locationsWrapper).removeClass(locationsDisplayClass)
537
  } else {
538
    $(locationsWrapper).addClass(locationsDisplayClass)
539
  }
540
}
541

    
542
function openDatepicker () {
543
  if ($(window).width() <= 990) {
544
    $('.navbar-collapse').collapse()
545
  }
546

    
547
  $('#date').datepicker('show')
548
}
549
function onDocumentReady () {
550
  $('#dataset-dropdown').on('click', function (e) {
551
    e.stopPropagation()
552
  })
553

    
554
  $('#submit-btn').prop('name', '')
555
  onValueChangeRegister()
556
}
557
const loadCheckboxDatasetNameData = () => {
558
  datasetSelected = []
559
  $('#dataset-dropdown .dropdown-item').each(function () {
560
    const input = $(this).find('input')
561
    const inputVal = input[0].value
562
    if (input[0].checked) {
563
      datasetSelected.push(inputVal)
564
    }
565
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
566
  })
567
}
(2-2/2)