Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 2f227a6c

Přidáno uživatelem Tomáš Ballák před více než 3 roky(ů)

Feature Re #8156 show all datasets

Zobrazit rozdíly:

website/public/js/zcu-heatmap.js
1 1
/* global L */
2 2
/* global $ */
3

  
3 4
var mymap
4 5
var heatmapLayer = null
5 6
var marksLayer = null
......
10 11

  
11 12
var dataSourceRoute
12 13
var currentTime
13
var name
14
var date
15 14

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

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

  
22
var datasetSelected = []
22 23

  
23
// holds all instances of markers for bind/unbind popup purpose
24
// contains: {key:[L.circle,L.pupup]}
25
// key: x and y, x + '' + y string
26 24
const globalMarkersHolder = {}
25
const datasetColorDict = {}
27 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
}
28 50
// all marker from which popup was removed
29 51
// contains: {key:[L.circle,L.pupup]}
30 52
// key: x and y, x + '' + y string
31 53
let globalMarkersChanged = {}
32 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
})
33 60
const genPopUpControls = (controls) => {
34
  return `<div class="popup-controls">${controls.reduce((sum, item) => sum + item, '')}</div>`
61
  return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>`
35 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

  
36 78
const genPopUp = (place, number, sum, currentPos, maxPos) => {
37 79
  const header = `<strong>Zařízení a počet:</strong><div id="place-info">${place}</div>`
38 80
  const currentNum = `<span id="digit-info">${number}</span>`
39 81
  // eslint-disable-next-line eqeqeq
40 82
  const sumNum = `<span id="total-info" style="font-size: large">${(sum && (sum != number)) ? '/' + sum : ''}</span>`
41 83
  const digitInfo = `<div id="number-info">${currentNum}${sumNum}</div>`
42
  let previousButton = '<button id="previous-info-btn" class="circle-button" onclick="previousInfo()"></button>'
43
  let nextButton = '<button id="next-info-btn" onclick="nextInfo()" class="circle-button next"></button>'
44
  let posInfo = `<div id="count-info">${currentPos} z ${maxPos}</div>`
45

  
46
  if (!sum) {
47
    previousButton = ''
48
    nextButton = ''
49
    posInfo = ''
50
  }
84
  const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos)
51 85
  return `
52 86
  ${header}
53 87
  ${digitInfo}
54
  ${genPopUpControls([previousButton, posInfo, nextButton])}
88
  ${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)}
55 89
  `
56 90
}
57 91
/**
......
72 106

  
73 107
  mymap.on('click', showInfo)
74 108
}
75

  
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
}
76 125
function showInfo (e) {
77 126
  info = []
78
  currenInfo = 0
127
  currentInfo = 0
79 128

  
80 129
  // https://wiki.openstreetmap.org/wiki/Zoom_levels
81 130
  // Todo change to variable - it is used in heatmap init
82
  var stile = 40075016.686 * Math.cos(startX) / Math.pow(2, mymap.getZoom())
83
  var radius = 25 * stile / 256
84

  
85
  var i = 0
86
  var lat = 0
87
  var lng = 0
88

  
89
  var total = 0
90
  data[currentTime].items.forEach(element => {
91
    if (e.latlng.distanceTo(new L.LatLng(element.x, element.y)) < radius) {
92
      lat += element.x
93
      lng += element.y
94
      info[i] = { place: element.place, number: element.number }
95
      total += parseInt(element.number)
96
      i++
97
    }
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
    })
98 154
  })
99 155

  
100
  if (info.length > 0) {
101
    const { place, number } = info[currenInfo]
102
    L.popup({
103
      autoPan: false
104
    })
105
      .setLatLng([lat / i, lng / i])
106
      .setContent(genPopUp(place, number, total, currenInfo + 1, info.length))
107
      .openOn(mymap)
156
  // Process info for more then one dataset
108 157

  
109
    if (info.length === 1) {
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) {
110 185
      $('#previous-info-btn').prop('disabled', true)
111 186
      $('#next-info-btn').prop('disabled', true)
112 187
      $('.popup-controls').hide()
113 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)
114 194
  }
115 195
}
116 196

  
117 197
// eslint-disable-next-line no-unused-vars
118 198
function previousInfo () {
119
  currenInfo = (currenInfo + info.length - 1) % info.length
120
  displayInfoText()
199
  const infoLength = getInfoLength()
200
  const previousCurrentInfo = currentInfo
201
  currentInfo = (currentInfo + infoLength - 1) % infoLength
202
  displayInfoText(previousCurrentInfo)
121 203
}
122 204

  
123 205
// eslint-disable-next-line no-unused-vars
124 206
function nextInfo () {
125
  currenInfo = (currenInfo + 1) % info.length
126
  displayInfoText()
207
  const infoLength = getInfoLength()
208
  const previousCurrentInfo = currentInfo
209
  currentInfo = (currentInfo + 1) % infoLength
210
  displayInfoText(previousCurrentInfo)
127 211
}
128

  
129
function displayInfoText () {
130
  $('#place-info').html(info[currenInfo].place)
131
  $('#digit-info').html(info[currenInfo].number)
132
  $('#count-info').html(currenInfo + 1 + ' z ' + info.length)
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}`)
133 221
}
134 222

  
135 223
// eslint-disable-next-line no-unused-vars
......
178 266
  updateHeaderControls()
179 267
  changeUrl()
180 268
}
181

  
269
const typeUrlReducer = (accumulator, currentValue) => accumulator + currentValue
182 270
/**
183 271
 * Change browser url based on animation step
184 272
 */
......
186 274
  window.history.pushState(
187 275
    '',
188 276
    document.title,
189
    window.location.origin + window.location.pathname + `?data_set[date]=${$('#date').val()}&data_set[time]=${currentTime}&data_set[type]=${$('#type').children('option:selected').val()}`
277
    window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
190 278
  )
191 279
}
192 280

  
......
205 293
 * @param {string} positionsRoute  route to dataset postitions source
206 294
 */
207 295
// eslint-disable-next-line no-unused-vars
208
function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
296
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) {
209 297
  dataSourceRoute = opendataRoute
210 298
  data = []
211

  
212
  name = $('#type').children('option:selected').val()
213
  date = $('#date').val()
299
  const dataSourceMarks = {}
300
  const allPromises = []
301
  const date = $('#date').val()
214 302
  currentTime = parseInt($('#time').children('option:selected').val())
215 303
  setTimeline()
216
  $.ajax({
217
    type: 'POST',
218
    url: positionsRoute + '/' + name,
219
    success: function (result) {
220
      drawDataSourceMarks(result)
221
      $.ajax({
222
        type: 'POST',
223
        url: dataSourceRoute + '/' + name + '/' + date + '/' + currentTime,
224
        success: function (result) {
225
          data[currentTime] = result
226
          drawHeatmap(data[currentTime])
227
        }
228
      })
229
    }
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))
230 313
  })
231

  
232
  preload(currentTime, 1)
233
  preload(currentTime, -1)
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
  )
234 322
}
235 323

  
236 324
function drawDataSourceMarks (data) {
......
238 326
    L.removeLayer(marksLayer)
239 327
  }
240 328
  marksLayer = L.layerGroup()
241
  for (var key in data) {
242
    const { x, y, name } = data[key]
243
    const pop =
244
      L.popup({ autoPan: false })
245
        .setLatLng([x, y])
246
        .setContent(genPopUp(name, 0, 0, 1, 1))
247
    const newCircle =
248
      L.circle([x, y], { radius: 2, fillOpacity: 0.8, color: '#004fb3', fillColor: '#004fb3', bubblingMouseEvents: true })
249
        .bindPopup(pop)
250
    globalMarkersHolder[x + '' + y] = [newCircle, pop] // add new marker to global holders
251
    marksLayer.addLayer(
252
      newCircle
253
    )
254
  }
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
  })
255 344

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

  
259
function preload (time, change) {
260
  var ntime = time + change
261
  if (ntime >= 0 && ntime <= 23) {
262
    $.ajax({
263
      type: 'POST',
264
      url: dataSourceRoute + '/' + name + '/' + date + '/' + ntime,
265
      success: function (result) {
266
        data[ntime] = result
267
        preload(ntime, change)
268
      }
269
    })
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
    }
270 356
  }
271 357
}
272 358

  
273
function drawHeatmap (data) {
359
function drawHeatmap (dataRaw) {
274 360
  // Todo still switched
275
  if (data.items != null) {
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) {
276 368
    // Bind back popups for markers (we dont know if there is any data for this marker or not)
277
    if (Object.keys(globalMarkersChanged).length) {
278
      Object.keys(globalMarkersChanged).forEach(function (key) {
279
        globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1])
280
      })
281
      globalMarkersChanged = {}
282
    }
283
    const points = data.items.map((point) => {
284
      const { x, y, number } = point
285
      const key = x + '' + y
286
      const holder = globalMarkersHolder[key]
287
      if (!globalMarkersChanged[key] && number) {
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) {
288 380
        // There is data for this marker => unbind popup with zero value
289
        holder[0] = holder[0].unbindPopup()
290
        globalMarkersChanged[key] = holder
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)
291 390
      }
292
      return [x, y, number]
293
    })
294
    if (heatmapLayer != null) {
295
      mymap.removeLayer(heatmapLayer)
296
    }
297
    heatmapLayer = L.heatLayer(points, { max: data.max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap)
298
  } else {
299
    if (heatmapLayer != null) {
300
      mymap.removeLayer(heatmapLayer)
301 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)
302 398
  }
303

  
304
  // var heat_01 = ...
305
  // on background map.addLayer(heat_01) -> map.removeLayer(heat_01);
306
  // $(.leaflet-heatmap-layer).css('opacity', 'value');
307 399
}
308 400

  
309 401
/**
......
322 414
  })
323 415
}
324 416

  
325
var allOptionsDisabled = false
326

  
327 417
function updateAvailableDataSets (available) {
328
  var isOptionEnabled = true
329
  $('#type > option').each(function () {
330
    if ((this.value in available) === false) {
331
      $(this).prop('disabled', true)
332
      $(this).prop('selected', false)
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)
333 426
    } else {
334
      $(this).prop('disabled', false)
335
      if (allOptionsDisabled) {
336
        $(this).prop('selected', true)
337
        allOptionsDisabled = false
338
      }
339
      isOptionEnabled = false
427
      leastOneOptionEnabled = true
428
      $(this).removeClass('disabled')
340 429
    }
341 430
  })
342
  allOptionsDisabled = isOptionEnabled
343 431

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

  
347 435
function formatDate (date) {
......
403 491

  
404 492
  $('#date').datepicker('show')
405 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
}

Také k dispozici: Unified diff