Projekt

Obecné

Profil

Stáhnout (15.1 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
var currentTime
14

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

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

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

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

    
31
const fetchByNameDate = async (baseRoute, name, date, currentTime) => {
32
  const headers = new Headers()
33
  const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, {
34
    method: 'GET',
35
    headers: headers
36
  })
37
  const beforeJson = await fetch(myRequest)
38
  return beforeJson.json()
39
}
40
const fetchDataSourceMarks = async (positionRoute, datasetName) => {
41
  const headers = new Headers()
42
  const myRequest = new Request(positionRoute + '/' + datasetName, {
43
    method: 'GET',
44
    headers: headers
45
  })
46
  const beforeJson = await fetch(myRequest)
47
  return beforeJson.json()
48
}
49

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

    
73
const genPopUp = (datasetName, place, number, sum, currentPos, maxPos) => {
74
  const header = `<strong>${datasetName}</strong><div id="place-info">${place}</div>`
75
  const currentNum = `<span id="digit-info">${number}</span>`
76
  // eslint-disable-next-line eqeqeq
77
  const sumNum = `<span id="total-info" style="font-size: large">${(sum && (sum != number)) ? '/' + sum : ''}</span>`
78
  const digitInfo = `<div id="number-info">${currentNum}${sumNum}</div>`
79
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
80
  return `
81
  ${header}
82
  ${digitInfo}
83
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
84
  `
85
}
86
/**
87
 * Initialize leaflet map on start position which can be default or set based on user action
88
 */
89
// eslint-disable-next-line no-unused-vars
90
function initMap () {
91
  startX = localStorage.getItem('lat') || startX
92
  startY = localStorage.getItem('lng') || startY
93
  startZoom = localStorage.getItem('zoom') || startZoom
94

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

    
97
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
98
    attribution: '',
99
    maxZoom: 19
100
  }).addTo(mymap)
101

    
102
  mymap.on('click', showInfo)
103
}
104
const getInfoLength = () => {
105
  const infoKeys = Object.keys(info)
106
  if (infoKeys.length === 1) {
107
    // return number of records in one dataset (one dataset in area)
108
    return info[infoKeys[0]].items.length
109
  }
110
  // return number of datasets (agregation of all datasets in area)
111
  return infoKeys.length
112
}
113
const getElFromObjectInfo = (position) => {
114
  const keys = Object.keys(info)
115
  return info[keys[position]]
116
}
117
const hasInfoMultipleDatasets = () => {
118
  return Object.keys(info).length > 1
119
}
120
function showInfo (e) {
121
  info = []
122
  currentInfo = 0
123

    
124
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
125
  // Todo change to variable - it is used in heatmap init
126
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
127
  const radius = 25 * stile / 256
128

    
129
  let i = 0
130
  let lat = 0
131
  let lng = 0
132

    
133
  let total = 0
134

    
135
  const datasetsInRadius = {}
136

    
137
  Object.keys(data[currentTime]).forEach((key) => {
138
    const namedData = data[currentTime][key]
139
    namedData.items.forEach(element => {
140
      if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
141
        lat += element.x
142
        lng += element.y
143
        info[i] = { place: element.place, number: element.number, datasetName: key }
144
        total += parseInt(element.number)
145
        i++
146
        datasetsInRadius[key] = true
147
      }
148
    })
149
  })
150

    
151
  // Process info for more then one dataset
152

    
153
  info = info.reduce((acc, item) => {
154
    if (!acc[item.datasetName]) {
155
      acc[item.datasetName] = {
156
        items: [],
157
        number: 0,
158
        datasetName: item.datasetName
159
      }
160
    }
161
    acc[item.datasetName].items.push(item)
162
    acc[item.datasetName].number += Number(item.number)
163
    return acc
164
  }, {})
165

    
166
  // There is one dataset
167

    
168
  const numDatasets = Object.keys(datasetsInRadius).length
169

    
170
  if (!numDatasets) {
171
    return
172
  }
173

    
174
  if (numDatasets === 1) {
175
    const infoDict = getElFromObjectInfo(0)
176
    const info_ = infoDict.items
177
    const { place, number } = info_[currentInfo]
178
    prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`)
179
      .setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length))
180
      .openOn(mymap)
181
    if (info_.length === 1) {
182
      $('#previous-info-btn').prop('disabled', true)
183
      $('#next-info-btn').prop('disabled', true)
184
      $('.popup-controls').hide()
185
    }
186
  } else {
187
    const { datasetName, number } = getElFromObjectInfo(currentInfo)
188
    prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`)
189
      .setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName]))
190
      .openOn(mymap)
191
  }
192
}
193

    
194
// eslint-disable-next-line no-unused-vars
195
function previousInfo () {
196
  const infoLength = getInfoLength()
197
  const previousCurrentInfo = currentInfo
198
  currentInfo = (currentInfo + infoLength - 1) % infoLength
199
  displayInfoText(previousCurrentInfo)
200
}
201

    
202
// eslint-disable-next-line no-unused-vars
203
function nextInfo () {
204
  const infoLength = getInfoLength()
205
  const previousCurrentInfo = currentInfo
206
  currentInfo = (currentInfo + 1) % infoLength
207
  displayInfoText(previousCurrentInfo)
208
}
209
function displayInfoText (previousInfoNum) {
210
  const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum]
211
  const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo]
212
  const infoLength = getInfoLength()
213
  const datasetInfo = $('#dataset-info')
214
  if (datasetInfo) {
215
    $(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName])
216
  }
217
  $('#place-info').html(info_.place ? info_.place : info_.datasetName)
218
  $('#digit-info').html(info_.number)
219
  $('#count-info').html(currentInfo + 1 + ' z ' + infoLength)
220
  $('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`)
221
  $('.leaflet-popup').addClass(`popup-${info_.datasetName}`)
222
}
223

    
224
// eslint-disable-next-line no-unused-vars
225
function setMapView (latitude, longitude, zoom) {
226
  localStorage.setItem('lat', latitude)
227
  localStorage.setItem('lng', longitude)
228
  localStorage.setItem('zoom', zoom)
229
  mymap.setView([latitude, longitude], zoom)
230
}
231

    
232
/**
233
 * Change animation start from playing to stopped or the other way round
234
 */
235
// eslint-disable-next-line no-unused-vars
236
function changeAnimationState () {
237
  isAnimationRunning = !isAnimationRunning
238
  if (isAnimationRunning) {
239
    $('#play-pause').attr('class', 'pause')
240
    timer = setInterval(
241
      function () {
242
        next()
243
      },
244
      800
245
    )
246
  } else {
247
    clearTimeout(timer)
248
    $('#play-pause').attr('class', 'play')
249
  }
250
}
251

    
252
// eslint-disable-next-line no-unused-vars
253
function previous () {
254
  currentTime = (currentTime + 23) % 24
255
  drawHeatmap(data[currentTime])
256
  setTimeline()
257
  mymap.closePopup()
258
  updateHeaderControls()
259
  changeUrl()
260
}
261

    
262
function next () {
263
  currentTime = (currentTime + 1) % 24
264
  drawHeatmap(data[currentTime])
265
  setTimeline()
266
  mymap.closePopup()
267
  updateHeaderControls()
268
  changeUrl()
269
}
270
const typeUrlReducer = (accumulator, currentValue) => accumulator + currentValue
271
/**
272
 * Change browser url based on animation step
273
 */
274
function changeUrl () {
275
  window.history.pushState(
276
    '',
277
    document.title,
278
    window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
279
  )
280
}
281

    
282
function updateHeaderControls () {
283
  document.getElementById('time').value = currentTime
284
}
285

    
286
function setTimeline () {
287
  $('#timeline').text(currentTime + ':00')
288
  $('#timeline').attr('class', 'time hour-' + currentTime)
289
}
290

    
291
/**
292
 * Load and display heatmap layer for current data
293
 * @param {string} opendataRoute route to dataset source
294
 * @param {string} positionsRoute  route to dataset postitions source
295
 */
296
// eslint-disable-next-line no-unused-vars
297
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
298
  dataSourceRoute = opendataRoute
299
  data = []
300
  const dataSourceMarks = {}
301
  const allPromises = []
302
  const date = $('#date').val()
303
  currentTime = parseInt($('#time').children('option:selected').val())
304
  setTimeline()
305
  data[currentTime] = {}
306
  const dataSelectedHandler = async (datasetName) => {
307
    const marks = await fetchDataSourceMarks(positionsRoute, datasetName)
308
    const datasetData = await fetchByNameDate(dataSourceRoute, datasetName, date, currentTime)
309
    dataSourceMarks[datasetName] = marks
310
    data[currentTime][datasetName] = datasetData
311
  }
312
  await datasetSelected.forEach((datasetName) => {
313
    allPromises.push(dataSelectedHandler(datasetName))
314
  })
315
  Promise.all(allPromises).then(
316
    () => {
317
      drawDataSourceMarks(dataSourceMarks)
318
      drawHeatmap(data[currentTime])
319
      preload(currentTime, 1, date)
320
      preload(currentTime, -1, date)
321
    }
322
  )
323
}
324

    
325
function drawDataSourceMarks (data) {
326
  if (marksLayer != null) {
327
    L.removeLayer(marksLayer)
328
  }
329
  marksLayer = L.layerGroup()
330
  Object.keys(data).forEach((key_) => {
331
    for (var key in data[key_]) {
332
      const { x, y, name } = data[key_][key]
333
      const pop =
334
          prepareLayerPopUp(x, y, 1, `popup-${key_}`)
335
            .setContent(genPopUp(datasetDictNameDisplayName[key_], name, 0, 0, 1, 1))
336
      const newCircle =
337
        L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
338
          .bindPopup(pop)
339
      globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
340
      marksLayer.addLayer(
341
        newCircle
342
      )
343
    }
344
  })
345

    
346
  marksLayer.setZIndex(-1).addTo(mymap)
347
}
348

    
349
async function preload (time, change, date) {
350
  for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) {
351
    if (!data[nTime]) {
352
      data[nTime] = {}
353
      datasetSelected.forEach(async (datasetName) => {
354
        data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime)
355
      })
356
    }
357
  }
358
}
359

    
360
function drawHeatmap (dataRaw) {
361
  // Todo still switched
362
  const dataDict = dataRaw
363
  const mergedPoints = []
364
  let max = 0
365
  if (Object.keys(globalMarkersChanged).length) {
366
    Object.keys(globalMarkersChanged).forEach(function (key) {
367
      globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
368
    })
369
    globalMarkersChanged = {}
370
  }
371
  Object.keys(dataDict).forEach((key) => {
372
    const data = dataDict[key]
373
    max = Math.max(max, data.max)
374
    if (data != null) {
375
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
376
      const points = data.items.map((point) => {
377
        const { x, y, number } = point
378
        const key = x + '' + y
379
        const holder = globalMarkersHolder[key]
380
        if (!globalMarkersChanged[key] && number) {
381
        // There is data for this marker => unbind popup with zero value
382
          holder[0] = holder[0].unbindPopup()
383
          globalMarkersChanged[key] = holder
384
        }
385
        return [x, y, number]
386
      })
387
      mergedPoints.push(...points)
388
    } else {
389
      if (heatmapLayer != null) {
390
        mymap.removeLayer(heatmapLayer)
391
      }
392
    }
393
  })
394
  if (heatmapLayer != null) {
395
    mymap.removeLayer(heatmapLayer)
396
  }
397
  if (mergedPoints.length) {
398
    heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
399
  }
400
}
401

    
402
/**
403
 * Checks dataset availibility
404
 * @param {string} route authority for datasets availibility checks
405
 */
406
// eslint-disable-next-line no-unused-vars
407
function checkDataSetsAvailability (route) {
408
  $.ajax({
409
    type: 'POST',
410
    // Todo it might be good idea to change db collections format
411
    url: route + '/' + $('#date').val(),
412
    success: function (result) {
413
      updateAvailableDataSets(result)
414
    }
415
  })
416
}
417

    
418
function updateAvailableDataSets (available) {
419
  let leastOneOptionEnabled = false
420
  // datasetSelected = []
421
  $('#dataset-dropdown .dropdown-item').each(function () {
422
    const input = $(this).find('input')
423
    const inputVal = input[0].value
424
    if (!(inputVal in available)) {
425
      $(this).addClass('disabled')
426
      $(input).prop('checked', false)
427
    } else {
428
      leastOneOptionEnabled = true
429
      $(this).removeClass('disabled')
430
    }
431
  })
432

    
433
  $('#submit-btn').prop('disabled', !leastOneOptionEnabled)
434
}
435

    
436
function formatDate (date) {
437
  var day = String(date.getDate())
438
  var month = String(date.getMonth() + 1)
439

    
440
  if (day.length === 1) {
441
    day = '0' + day
442
  }
443

    
444
  if (month.length === 1) {
445
    month = '0' + month
446
  }
447

    
448
  return date.getFullYear() + '-' + month + '-' + day
449
}
450

    
451
// eslint-disable-next-line no-unused-vars
452
function initDatepicker (availableDatesSource) {
453
  var availableDates = ''
454

    
455
  $.ajax({
456
    type: 'GET',
457
    url: availableDatesSource,
458
    success: function (result) {
459
      availableDates = String(result).split(',')
460
    }
461
  }).then(function () {
462
    $('#date').datepicker({
463
      format: 'yyyy-mm-dd',
464
      language: 'cs',
465
      beforeShowDay: function (date) {
466
        if (availableDates.indexOf(formatDate(date)) < 0) {
467
          return { enabled: false, tooltip: 'Žádná data' }
468
        } else {
469
          return { enabled: true }
470
        }
471
      },
472
      autoclose: true
473
    })
474
  })
475
}
476

    
477
function initLocationsMenu () {
478
  var locationsWrapper = '.locations'
479
  var locationsDisplayClass = 'show'
480

    
481
  if ($(window).width() <= 480) {
482
    $(locationsWrapper).removeClass(locationsDisplayClass)
483
  } else {
484
    $(locationsWrapper).addClass(locationsDisplayClass)
485
  }
486
}
487

    
488
function openDatepicker () {
489
  if ($(window).width() <= 990) {
490
    $('.navbar-collapse').collapse()
491
  }
492

    
493
  $('#date').datepicker('show')
494
}
495
function onDocumentReady () {
496
  $('#dataset-dropdown').on('click', function (e) {
497
    e.stopPropagation()
498
  })
499
  datasetSelected = []
500
  $('#dataset-dropdown .dropdown-item').each(function () {
501
    const input = $(this).find('input')
502
    const inputVal = input[0].value
503
    if (input[0].checked) {
504
      datasetSelected.push(inputVal)
505
    }
506
    datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name')
507
  })
508
  $('#submit-btn').prop('name', '')
509
}
(2-2/2)