Revize c892003d
Přidáno uživatelem Martin Sebela před více než 3 roky(ů)
website/public/js/zcu-heatmap.js | ||
---|---|---|
29 | 29 |
// key: x and y, x + '' + y string |
30 | 30 |
let globalMarkersChanged = {} |
31 | 31 |
|
32 |
|
|
32 | 33 |
const fetchByNameDate = async (baseRoute, name, date, currentTime) => { |
33 | 34 |
const headers = new Headers() |
34 | 35 |
const myRequest = new Request(baseRoute + '/' + name + '/' + date + '/' + currentTime, { |
... | ... | |
38 | 39 |
const beforeJson = await fetch(myRequest) |
39 | 40 |
return beforeJson.json() |
40 | 41 |
} |
42 |
|
|
43 |
|
|
41 | 44 |
const fetchDataSourceMarks = async (positionRoute, datasetName) => { |
42 | 45 |
const headers = new Headers() |
43 | 46 |
const myRequest = new Request(positionRoute + '/' + datasetName, { |
... | ... | |
48 | 51 |
return beforeJson.json() |
49 | 52 |
} |
50 | 53 |
|
54 |
|
|
51 | 55 |
const genPopUpControlButtons = (currentPage, numPages, onNextClick, onPreviousClick) => ({ |
52 | 56 |
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>',
|
|
57 |
nextButton: '<button id="next-info-btn" class="circle-button next" onclick="nextInfo()"></button>',
|
|
54 | 58 |
posInfo: `<div id="count-info">${currentPage} z ${numPages}</div>` |
55 | 59 |
}) |
60 |
|
|
61 |
|
|
56 | 62 |
const genPopUpControls = (controls) => { |
57 | 63 |
return `<div class="popup-controls">${controls ? controls.reduce((sum, item) => sum + item, '') : ''}</div>` |
58 | 64 |
} |
65 |
|
|
66 |
|
|
59 | 67 |
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>`
|
|
68 |
const popupHeader = `<strong id="dataset-info">${datasetName}</strong>`
|
|
69 |
const popupData = `<div id="number-info"><span id="digit-info">${sum}</span></div>`
|
|
62 | 70 |
const { previousButton, nextButton, posInfo } = genPopUpControlButtons(currentPos, maxPos) |
71 |
|
|
63 | 72 |
return ` |
64 |
${header}
|
|
65 |
${digitInfo}
|
|
73 |
${popupHeader}
|
|
74 |
${popupData}
|
|
66 | 75 |
${genPopUpControls([previousButton, posInfo, nextButton])} |
67 | 76 |
` |
68 | 77 |
} |
78 |
|
|
79 |
|
|
69 | 80 |
const prepareLayerPopUp = (lat, lng, num, className) => L.popup({ |
70 | 81 |
autoPan: false, |
71 | 82 |
className: className |
72 | 83 |
}).setLatLng([lat / num, lng / num]) |
73 | 84 |
|
85 |
|
|
74 | 86 |
const genPopUp = (datasetName, place, count, sum, currentPos, maxPos) => { |
75 | 87 |
const popupHeader = ` |
76 | 88 |
<strong>${datasetName}</strong> |
... | ... | |
88 | 100 |
${genPopUpControls(maxPos > 1 ? [previousButton, posInfo, nextButton] : null)} |
89 | 101 |
` |
90 | 102 |
} |
103 |
|
|
104 |
|
|
91 | 105 |
const onCheckboxClicked = async (checkbox) => { |
92 | 106 |
if ($(checkbox).prop('checked')) { |
93 | 107 |
loadCurrentTimeHeatmap(dataSourceRoute, positionsSourceRoute) |
94 | 108 |
changeUrl() |
95 |
} else { |
|
109 |
} |
|
110 |
else { |
|
96 | 111 |
loadCheckboxDatasetNameData() |
112 |
|
|
97 | 113 |
data.forEach((item, index) => { |
98 | 114 |
Object.keys(item).forEach((datasetName) => { |
99 | 115 |
if (datasetName === $(checkbox).val()) { |
... | ... | |
102 | 118 |
}) |
103 | 119 |
drawHeatmap(data[currentTime]) |
104 | 120 |
}) |
121 |
|
|
105 | 122 |
changeUrl() |
106 | 123 |
} |
107 | 124 |
} |
125 |
|
|
126 |
|
|
108 | 127 |
const debounce = (func, delay) => { |
109 | 128 |
let inDebounce |
110 | 129 |
return function () { |
... | ... | |
115 | 134 |
} |
116 | 135 |
} |
117 | 136 |
|
137 |
|
|
118 | 138 |
const onValueChangeRegister = () => { |
119 | 139 |
$('#date').change(function () { |
120 | 140 |
data = [] |
... | ... | |
124 | 144 |
changeUrl() |
125 | 145 |
}) |
126 | 146 |
|
127 |
$('#dataset-dropdown-time input[type="radio"]').each(function () {
|
|
147 |
$('#dropdown-time input[type="radio"]').each(function () { |
|
128 | 148 |
$(this).change(function () { |
129 | 149 |
currentTime = parseInt($(this).val()) |
130 | 150 |
updateHeaderControls() |
... | ... | |
134 | 154 |
}) |
135 | 155 |
}) |
136 | 156 |
|
137 |
$('input[type=checkbox]').each(function () {
|
|
157 |
$('#dropdown-dataset input[type="checkbox"]').each(function () {
|
|
138 | 158 |
$(this).change( |
139 | 159 |
debounce(() => onCheckboxClicked(this), 1000) |
140 | 160 |
) |
141 | 161 |
}) |
142 | 162 |
} |
143 | 163 |
|
164 |
|
|
144 | 165 |
/** |
145 | 166 |
* Initialize leaflet map on start position which can be default or set based on user action |
146 | 167 |
*/ |
... | ... | |
159 | 180 |
|
160 | 181 |
mymap.on('click', showInfo) |
161 | 182 |
} |
183 |
|
|
184 |
|
|
162 | 185 |
const getInfoLength = () => { |
163 | 186 |
const infoKeys = Object.keys(info) |
164 | 187 |
if (infoKeys.length === 1) { |
... | ... | |
168 | 191 |
// return number of datasets (agregation of all datasets in area) |
169 | 192 |
return infoKeys.length |
170 | 193 |
} |
194 |
|
|
195 |
|
|
171 | 196 |
const getElFromObjectInfo = (position) => { |
172 | 197 |
const keys = Object.keys(info) |
173 | 198 |
return info[keys[position]] |
174 | 199 |
} |
200 |
|
|
201 |
|
|
175 | 202 |
const hasInfoMultipleDatasets = () => { |
176 | 203 |
return Object.keys(info).length > 1 |
177 | 204 |
} |
205 |
|
|
206 |
|
|
178 | 207 |
function showInfo (e) { |
179 | 208 |
info = [] |
180 | 209 |
currentInfo = 0 |
... | ... | |
216 | 245 |
datasetName: item.datasetName |
217 | 246 |
} |
218 | 247 |
} |
248 |
|
|
219 | 249 |
acc[item.datasetName].items.push(item) |
220 | 250 |
acc[item.datasetName].number += Number(item.number) |
221 | 251 |
return acc |
... | ... | |
236 | 266 |
prepareLayerPopUp(lat, lng, i, `popup-${infoDict.datasetName}`) |
237 | 267 |
.setContent(genPopUp(datasetDictNameDisplayName[infoDict.datasetName], place, number, total, currentInfo + 1, info_.length)) |
238 | 268 |
.openOn(mymap) |
269 |
|
|
239 | 270 |
if (info_.length === 1) { |
240 | 271 |
$('#previous-info-btn').prop('disabled', true) |
241 | 272 |
$('#next-info-btn').prop('disabled', true) |
242 | 273 |
$('.popup-controls').hide() |
243 | 274 |
} |
244 |
} else { |
|
275 |
} |
|
276 |
else { |
|
245 | 277 |
const { datasetName, number } = getElFromObjectInfo(currentInfo) |
278 |
|
|
246 | 279 |
prepareLayerPopUp(lat, lng, i, `popup-${datasetName}`) |
247 | 280 |
.setContent(genMultipleDatasetsPopUp(number, currentInfo + 1, getInfoLength(), datasetDictNameDisplayName[datasetName])) |
248 | 281 |
.openOn(mymap) |
249 | 282 |
} |
250 | 283 |
} |
251 | 284 |
|
285 |
|
|
252 | 286 |
// eslint-disable-next-line no-unused-vars |
253 | 287 |
function previousInfo () { |
254 | 288 |
const infoLength = getInfoLength() |
255 | 289 |
const previousCurrentInfo = currentInfo |
290 |
|
|
256 | 291 |
currentInfo = (currentInfo + infoLength - 1) % infoLength |
257 | 292 |
displayInfoText(previousCurrentInfo) |
258 | 293 |
} |
259 | 294 |
|
295 |
|
|
260 | 296 |
// eslint-disable-next-line no-unused-vars |
261 | 297 |
function nextInfo () { |
262 | 298 |
const infoLength = getInfoLength() |
263 | 299 |
const previousCurrentInfo = currentInfo |
300 |
|
|
264 | 301 |
currentInfo = (currentInfo + 1) % infoLength |
265 | 302 |
displayInfoText(previousCurrentInfo) |
266 | 303 |
} |
304 |
|
|
305 |
|
|
267 | 306 |
function displayInfoText (previousInfoNum) { |
268 | 307 |
const previousInfo = hasInfoMultipleDatasets() ? getElFromObjectInfo(previousInfoNum) : getElFromObjectInfo(0).items[previousInfoNum] |
269 | 308 |
const info_ = hasInfoMultipleDatasets() ? getElFromObjectInfo(currentInfo) : getElFromObjectInfo(0).items[currentInfo] |
270 | 309 |
const infoLength = getInfoLength() |
271 | 310 |
const datasetInfo = $('#dataset-info') |
311 |
|
|
272 | 312 |
if (datasetInfo) { |
273 | 313 |
$(datasetInfo).html(datasetDictNameDisplayName[info_.datasetName]) |
274 | 314 |
} |
315 |
|
|
275 | 316 |
$('#place-info').html(info_.place ? info_.place : info_.datasetName) |
276 | 317 |
$('#digit-info').html(info_.number) |
277 | 318 |
$('#count-info').html(currentInfo + 1 + ' z ' + infoLength) |
319 |
|
|
278 | 320 |
$('.leaflet-popup').removeClass(`popup-${previousInfo.datasetName}`) |
279 | 321 |
$('.leaflet-popup').addClass(`popup-${info_.datasetName}`) |
280 | 322 |
} |
281 | 323 |
|
324 |
|
|
282 | 325 |
// eslint-disable-next-line no-unused-vars |
283 | 326 |
function setMapView (latitude, longitude, zoom) { |
284 | 327 |
localStorage.setItem('lat', latitude) |
... | ... | |
287 | 330 |
mymap.setView([latitude, longitude], zoom) |
288 | 331 |
} |
289 | 332 |
|
333 |
|
|
290 | 334 |
/** |
291 | 335 |
* Change animation start from playing to stopped or the other way round |
292 | 336 |
*/ |
293 | 337 |
// eslint-disable-next-line no-unused-vars |
294 | 338 |
function changeAnimationState () { |
295 | 339 |
isAnimationRunning = !isAnimationRunning |
340 |
|
|
296 | 341 |
if (isAnimationRunning) { |
297 | 342 |
$('#play-pause').attr('class', 'pause') |
298 |
timer = setInterval( |
|
299 |
function () { |
|
300 |
next() |
|
301 |
}, |
|
302 |
800 |
|
303 |
) |
|
304 |
} else { |
|
343 |
timer = setInterval(function() { next() }, 800) |
|
344 |
} |
|
345 |
else { |
|
305 | 346 |
clearTimeout(timer) |
306 | 347 |
$('#play-pause').attr('class', 'play') |
307 | 348 |
} |
308 | 349 |
} |
309 | 350 |
|
351 |
|
|
310 | 352 |
// eslint-disable-next-line no-unused-vars |
311 | 353 |
function previous () { |
312 | 354 |
currentTime = (currentTime + 23) % 24 |
... | ... | |
317 | 359 |
changeUrl() |
318 | 360 |
} |
319 | 361 |
|
362 |
|
|
320 | 363 |
function next () { |
321 | 364 |
currentTime = (currentTime + 1) % 24 |
322 | 365 |
drawHeatmap(data[currentTime]) |
... | ... | |
325 | 368 |
updateHeaderControls() |
326 | 369 |
changeUrl() |
327 | 370 |
} |
328 |
const typeUrlReducer = (accumulator, currentValue) => accumulator + currentValue |
|
371 |
|
|
372 |
|
|
329 | 373 |
/** |
330 |
* Change browser url based on animation step |
|
374 |
* Change browser url based on animation step.
|
|
331 | 375 |
*/ |
332 | 376 |
function changeUrl () { |
333 | 377 |
window.history.pushState( |
334 | 378 |
'', |
335 | 379 |
document.title, |
336 |
window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type[]=' + current, '')}`
|
|
380 |
window.location.origin + window.location.pathname + `?date=${$('#date').val()}&time=${currentTime}${datasetSelected.reduce((acc, current) => acc + '&type=' + current, '')}` |
|
337 | 381 |
) |
338 | 382 |
} |
339 | 383 |
|
384 |
|
|
340 | 385 |
function updateHeaderControls () { |
341 | 386 |
$(`#time_${currentTime}`).prop('checked', true) |
342 | 387 |
$('#dropdownMenuButtonTime').html((currentTime < 10 ? '0' : '') + `${currentTime}:00`) |
343 | 388 |
} |
344 | 389 |
|
390 |
|
|
345 | 391 |
function setTimeline () { |
346 | 392 |
$('#timeline').text(currentTime + ':00') |
347 | 393 |
$('#timeline').attr('class', 'time hour-' + currentTime) |
348 | 394 |
} |
349 | 395 |
|
396 |
|
|
397 |
function changeHour(hour) { |
|
398 |
currentTime = hour |
|
399 |
updateHeaderControls() |
|
400 |
setTimeline() |
|
401 |
drawHeatmap(data[currentTime]) |
|
402 |
changeUrl() |
|
403 |
} |
|
404 |
|
|
405 |
|
|
350 | 406 |
/** |
351 | 407 |
* Load and display heatmap layer for current data |
352 | 408 |
* @param {string} opendataRoute route to dataset source |
... | ... | |
355 | 411 |
// eslint-disable-next-line no-unused-vars |
356 | 412 |
async function loadCurrentTimeHeatmap (opendataRoute, positionsRoute) { |
357 | 413 |
loadCheckboxDatasetNameData() |
414 |
|
|
358 | 415 |
dataSourceRoute = opendataRoute |
359 | 416 |
positionsSourceRoute = positionsRoute |
360 | 417 |
const dataSourceMarks = {} |
361 | 418 |
const allPromises = [] |
362 | 419 |
const date = $('#date').val() |
363 |
currentTime = parseInt($('#dataset-dropdown-time input[type="radio"]:checked').val())
|
|
420 |
currentTime = parseInt($('#dropdown-time input[type="radio"]:checked').val()) |
|
364 | 421 |
|
365 | 422 |
setTimeline() |
366 | 423 |
data[currentTime] = {} |
... | ... | |
370 | 427 |
dataSourceMarks[datasetName] = marks |
371 | 428 |
data[currentTime][datasetName] = datasetData |
372 | 429 |
} |
430 |
|
|
373 | 431 |
await datasetSelected.forEach((datasetName) => { |
374 | 432 |
allPromises.push(dataSelectedHandler(datasetName)) |
375 | 433 |
}) |
434 |
|
|
376 | 435 |
Promise.all(allPromises).then( |
377 | 436 |
() => { |
378 | 437 |
drawDataSourceMarks(dataSourceMarks) |
... | ... | |
383 | 442 |
) |
384 | 443 |
} |
385 | 444 |
|
445 |
|
|
386 | 446 |
function drawDataSourceMarks (data) { |
387 | 447 |
if (marksLayer != null) { |
388 | 448 |
mymap.removeLayer(marksLayer) |
389 | 449 |
} |
450 |
|
|
390 | 451 |
marksLayer = L.layerGroup() |
452 |
|
|
391 | 453 |
Object.keys(data).forEach((key_) => { |
392 | 454 |
for (var key in data[key_]) { |
393 | 455 |
const { x, y, name } = data[key_][key] |
... | ... | |
407 | 469 |
marksLayer.setZIndex(-1).addTo(mymap) |
408 | 470 |
} |
409 | 471 |
|
472 |
|
|
410 | 473 |
async function preload (time, change, date) { |
411 | 474 |
for (let nTime = time + change; nTime >= 0 && nTime <= 23; nTime = nTime + change) { |
412 | 475 |
if (!data[nTime]) { |
413 | 476 |
data[nTime] = {} |
414 | 477 |
} |
478 |
|
|
415 | 479 |
datasetSelected.forEach(async (datasetName) => { |
416 | 480 |
if (!data[nTime][datasetName]) { |
417 | 481 |
data[nTime][datasetName] = await fetchByNameDate(dataSourceRoute, datasetName, date, nTime) |
... | ... | |
420 | 484 |
} |
421 | 485 |
} |
422 | 486 |
|
487 |
|
|
423 | 488 |
function drawHeatmap (dataRaw) { |
424 | 489 |
// Todo still switched |
425 | 490 |
const dataDict = dataRaw |
426 | 491 |
const mergedPoints = [] |
427 | 492 |
let max = 0 |
493 |
|
|
428 | 494 |
if (Object.keys(globalMarkersChanged).length) { |
429 | 495 |
Object.keys(globalMarkersChanged).forEach(function (key) { |
430 | 496 |
globalMarkersChanged[key][0].bindPopup(globalMarkersChanged[key][1]) |
431 | 497 |
}) |
432 | 498 |
globalMarkersChanged = {} |
433 | 499 |
} |
500 |
|
|
434 | 501 |
Object.keys(dataDict).forEach((key) => { |
435 | 502 |
const data = dataDict[key] |
436 | 503 |
max = Math.max(max, data.max) |
504 |
|
|
437 | 505 |
if (data != null) { |
438 | 506 |
// Bind back popups for markers (we dont know if there is any data for this marker or not) |
439 | 507 |
const points = data.items.map((point) => { |
440 | 508 |
const { x, y, number } = point |
441 | 509 |
const key = x + '' + y |
442 | 510 |
const holder = globalMarkersHolder[key] |
511 |
|
|
443 | 512 |
if (!globalMarkersChanged[key] && number) { |
444 | 513 |
// There is data for this marker => unbind popup with zero value |
445 | 514 |
holder[0] = holder[0].unbindPopup() |
446 | 515 |
globalMarkersChanged[key] = holder |
447 | 516 |
} |
517 |
|
|
448 | 518 |
return [x, y, number] |
449 | 519 |
}) |
450 | 520 |
mergedPoints.push(...points) |
451 |
} else { |
|
521 |
} |
|
522 |
else { |
|
452 | 523 |
if (heatmapLayer != null) { |
453 | 524 |
mymap.removeLayer(heatmapLayer) |
454 | 525 |
} |
455 | 526 |
} |
456 | 527 |
}) |
528 |
|
|
457 | 529 |
if (heatmapLayer != null) { |
458 | 530 |
mymap.removeLayer(heatmapLayer) |
459 | 531 |
} |
532 |
|
|
460 | 533 |
if (mergedPoints.length) { |
461 | 534 |
heatmapLayer = L.heatLayer(mergedPoints, { max: max, minOpacity: 0.5, radius: 35, blur: 30 }).addTo(mymap) |
462 | 535 |
} |
463 | 536 |
} |
464 | 537 |
|
538 |
|
|
465 | 539 |
/** |
466 | 540 |
* Checks dataset availibility |
467 | 541 |
* @param {string} route authority for datasets availibility checks |
... | ... | |
478 | 552 |
}) |
479 | 553 |
} |
480 | 554 |
|
555 |
|
|
481 | 556 |
function updateAvailableDataSets (available) { |
482 | 557 |
let leastOneOptionEnabled = false |
483 | 558 |
// datasetSelected = [] |
484 |
$('#dataset-dropdown .dropdown-item').each(function () { |
|
559 |
|
|
560 |
$('#dropdown-dataset .dropdown-item').each(function () { |
|
485 | 561 |
const input = $(this).find('input') |
486 | 562 |
const inputVal = input[0].value |
563 |
|
|
487 | 564 |
if (!(inputVal in available)) { |
488 | 565 |
$(this).addClass('disabled') |
489 | 566 |
$(input).prop('checked', false) |
490 |
} else { |
|
567 |
} |
|
568 |
else { |
|
491 | 569 |
leastOneOptionEnabled = true |
492 | 570 |
$(this).removeClass('disabled') |
493 | 571 |
} |
... | ... | |
496 | 574 |
$('#btn-update-heatmap').prop('disabled', !leastOneOptionEnabled) |
497 | 575 |
} |
498 | 576 |
|
577 |
|
|
499 | 578 |
function formatDate (date) { |
500 | 579 |
var day = String(date.getDate()) |
501 | 580 |
var month = String(date.getMonth() + 1) |
... | ... | |
511 | 590 |
return date.getFullYear() + '-' + month + '-' + day |
512 | 591 |
} |
513 | 592 |
|
593 |
|
|
514 | 594 |
// eslint-disable-next-line no-unused-vars |
515 | 595 |
function initDatepicker (availableDatesSource) { |
516 | 596 |
var availableDates = '' |
... | ... | |
528 | 608 |
beforeShowDay: function (date) { |
529 | 609 |
if (availableDates.indexOf(formatDate(date)) < 0) { |
530 | 610 |
return { enabled: false, tooltip: 'Žádná data' } |
531 |
} else { |
|
611 |
} |
|
612 |
else { |
|
532 | 613 |
return { enabled: true } |
533 | 614 |
} |
534 | 615 |
}, |
... | ... | |
537 | 618 |
}) |
538 | 619 |
} |
539 | 620 |
|
621 |
|
|
540 | 622 |
function initLocationsMenu () { |
541 | 623 |
var locationsWrapper = '.locations' |
542 | 624 |
var locationsDisplayClass = 'show' |
543 | 625 |
|
544 | 626 |
if ($(window).width() <= 480) { |
545 | 627 |
$(locationsWrapper).removeClass(locationsDisplayClass) |
546 |
} else { |
|
628 |
} |
|
629 |
else { |
|
547 | 630 |
$(locationsWrapper).addClass(locationsDisplayClass) |
548 | 631 |
} |
549 | 632 |
} |
550 | 633 |
|
634 |
|
|
551 | 635 |
function openDatepicker () { |
552 | 636 |
if ($(window).width() <= 990) { |
553 | 637 |
$('.navbar-collapse').collapse() |
... | ... | |
555 | 639 |
|
556 | 640 |
$('#date').datepicker('show') |
557 | 641 |
} |
642 |
|
|
643 |
|
|
558 | 644 |
function onDocumentReady () { |
559 |
$('#dataset-dropdown').on('click', function (e) {
|
|
645 |
$('#dropdown-dataset').on('click', function (e) {
|
|
560 | 646 |
e.stopPropagation() |
561 | 647 |
}) |
562 | 648 |
|
563 | 649 |
$('#btn-update-heatmap').prop('name', '') |
564 | 650 |
onValueChangeRegister() |
565 | 651 |
} |
652 |
|
|
653 |
|
|
566 | 654 |
const loadCheckboxDatasetNameData = () => { |
567 | 655 |
datasetSelected = [] |
568 |
$('#dataset-dropdown .dropdown-item').each(function () {
|
|
656 |
$('#dropdown-dataset .dropdown-item').each(function () {
|
|
569 | 657 |
const input = $(this).find('input') |
570 | 658 |
const inputVal = input[0].value |
659 |
|
|
571 | 660 |
if (input[0].checked) { |
572 | 661 |
datasetSelected.push(inputVal) |
573 | 662 |
} |
663 |
|
|
574 | 664 |
datasetDictNameDisplayName[inputVal] = $(input).data('dataset-display-name') |
575 | 665 |
}) |
576 | 666 |
} |
Také k dispozici: Unified diff
Re #8159 - CSS improvements, timeline clickable, code refactoring