Projekt

Obecné

Profil

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

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

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

    
22
var datasetSelected = []
23

    
24
const globalMarkersHolder = {}
25
const datasetColorDict = {}
26

    
27
/* const genColor = (datasetNames) => {
28
  datasetNames.forEach((name) => {
29
    datasetColorDict[name] =
30
  })
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
// all marker from which popup was removed
51
// contains: {key:[L.circle,L.pupup]}
52
// key: x and y, x + '' + y string
53
let globalMarkersChanged = {}
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" onclick="nextInfo()" class="circle-button next"></button>',
58
  posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>`
59
})
60
const genPopUpControls = (controls) => {
61
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
62
}
63
const multipleDatasetsPopUp = (sum, currentPos, maxPos, datasetName) => {
64
  const header = `<strong>Dataset a počet:</strong><div id="place-info">${datasetName}</div>`
65
  const digitInfo = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
66
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
67
  return `
68
  ${header}
69
  ${digitInfo}
70
  ${genPopUpControls([previousButton, posInfo, nextButton])}
71
  `
72
}
73
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({
74
  autoPan: false,
75
  className: className
76
}).setLatLng([lat / num, lng / num])
77

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

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

    
102
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
103
    attribution: '',
104
    maxZoom: 19
105
  }).addTo(mymap)
106

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

    
129
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
130
  // Todo change to variable - it is used in heatmap init
131
  const stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
132
  const radius = 25 * stile / 256
133

    
134
  let i = 0
135
  let lat = 0
136
  let lng = 0
137

    
138
  let total = 0
139

    
140
  const datasetsInRadius = {}
141

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

    
156
  // Process info for more then one dataset
157

    
158
  info = info.reduce((acc, item) => {
159
    if (!acc[item.datasetName]) {
160
      acc[item.datasetName] = {
161
        items: [],
162
        number: 0,
163
        datasetName: item.datasetName
164
      }
165
    }
166
    acc[item.datasetName].items.push(item)
167
    acc[item.datasetName].number += Number(item.number)
168
    return acc
169
  }, {})
170

    
171
  // There is one dataset
172

    
173
  const numDatasets = Object.keys(datasetsInRadius).length
174

    
175
  if (!numDatasets) { return }
176

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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