Projekt

Obecné

Profil

Stáhnout (14.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
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
// all marker from which popup was removed
26
// contains: {key:[L.circle,L.pupup]}
27
// key: x and y, x + '' + y string
28
let globalMarkersChanged = {}
29

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

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

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

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

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

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

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

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

    
132
  let total = 0
133

    
134
  const datasetsInRadius = {}
135

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

    
150
  // Process info for more then one dataset
151

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

    
165
  // There is one dataset
166

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

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

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

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

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

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

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

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

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

    
277
function updateHeaderControls () {
278
  document.getElementById('time').value = currentTime
279
}
280

    
281
function setTimeline () {
282
  $('#timeline').text(currentTime + ':00')
283
  $('#timeline').attr('class', 'time hour-' + currentTime)
284
}
285

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

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

    
341
  marksLayer.setZIndex(-1).addTo(mymap)
342
}
343

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

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

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

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

    
428
  $('#submit-btn').prop('disabled', !leastOneOptionEnabled)
429
}
430

    
431
function formatDate (date) {
432
  var day = String(date.getDate())
433
  var month = String(date.getMonth() + 1)
434

    
435
  if (day.length === 1) {
436
    day = '0' + day
437
  }
438

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

    
443
  return date.getFullYear() + '-' + month + '-' + day
444
}
445

    
446
// eslint-disable-next-line no-unused-vars
447
function initDatepicker (availableDatesSource) {
448
  var availableDates = ''
449

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

    
472
function initLocationsMenu () {
473
  var locationsWrapper = '.locations'
474
  var locationsDisplayClass = 'show'
475

    
476
  if ($(window).width() <= 480) {
477
    $(locationsWrapper).removeClass(locationsDisplayClass)
478
  } else {
479
    $(locationsWrapper).addClass(locationsDisplayClass)
480
  }
481
}
482

    
483
function openDatepicker () {
484
  if ($(window).width() <= 990) {
485
    $('.navbar-collapse').collapse()
486
  }
487

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