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:

modules/crawler/DatasetConfigs/JIS.yaml
1 1
# jmeno datasetu, pod kterym bude zobrazen v aplikaci
2 2
display-name: Snímače JIS karet
3
# barva pro dataset
4
display-color: '#0B155A'
3 5
# jednoslovný název datasetu, pod kterym bude reprezentovana v architektuře
4 6
dataset-name: JIS
5 7
# root slozka, ktera obsahuje odkazy na dataset
modules/crawler/DatasetConfigs/KOLOBEZKY.yaml
1 1
# jmeno datasetu, pod kterym bude zobrazen v aplikaci
2 2
display-name: Půjčování koloběžek
3
# barva pro dataset
4
display-color: '#0048A9'
3 5
# jednoslovný název datasetu, pod kterym bude reprezentovana v architektuře
4 6
dataset-name: KOLOBEZKY
5 7
# root slozka, ktera obsahuje odkazy na dataset
modules/crawler/DatasetConfigs/OBSAZENIMISTNOSTI.yaml
1 1
# jmeno datasetu, pod kterym bude zobrazen v aplikaci
2 2
display-name: Obsazení místností
3
# barva pro dataset
4
display-color: '#2c3e50'
3 5
# jednoslovný název datasetu, pod kterym bude reprezentovana v architektuře
4 6
dataset-name: OBSAZENIMISTNOSTI
5 7
# root slozka, ktera obsahuje odkazy na dataset
......
995 997

  
996 998
  - HJ-218:
997 999
      x: UNKNOWN!
998
      y: UNKNOWN!
1000
      y: UNKNOWN!
1001
  - CD-212:
1002
      x: UNKNOWN!
1003
      y: UNKNOWN!
1004

  
modules/crawler/DatasetConfigs/WIFI.yaml
1 1
# jmeno datasetu, pod kterym bude zobrazen v aplikaci
2 2
display-name: Počítačová síť ZČU
3
# barva pro dataset
4
display-color: '#574b90'
3 5
# jednoslovný název datasetu, pod kterym bude reprezentovana v architektuře
4 6
dataset-name: WIFI
5 7
# root slozka, ktera obsahuje odkazy na dataset
modules/crawler/Utilities/Database/database_loader.py
1 1
from Utilities.Database import database_data_line, database_record_logs
2 2
from Utilities import configure_functions
3
from Utilities.helpers import should_skip
3
from Utilities.helpers import should_skip, detect_change
4 4
import pymongo
5 5
import re
6 6

  
......
24 24
def create_database_connection():
25 25
    """
26 26
    Creates connection to mongoDB
27
    
27

  
28 28
    Returns:
29 29
        Connection to mongoDB
30 30
    """
......
116 116
        config: loaded configuration file of dataset
117 117
    """
118 118
    # collection where are specified aviable datasets
119
    compareKeys = ['display-name',
120
                   'display-color']
119 121
    collection_datasets = database_connection[MONGODB_DATASET_COLLECTION]
120 122

  
121
    dataset_name = config['dataset-name']
122
    display_name = config['display-name']
123

  
124
    query = {'key-name': dataset_name}
123
    query = {'key-name': config['dataset-name']}
125 124

  
126 125
    # check if newly added data already have a dataset specified in collection
127
    dataset_present = collection_datasets.find_one(query)
126
    current_dataset = collection_datasets.find_one(query)
128 127

  
129
    if dataset_present is None:
128
    if current_dataset is None:
130 129
        collection_datasets.insert_one({
131
            'key-name': dataset_name,
132
            'display-name': display_name,
130
            'key-name': config['dataset-name'],
131
            'display-name': config['display-name'],
132
            'display-color': config['display-color'],
133 133
            'updated': 0
134 134
        })
135
    elif dataset_present['display-name'] != display_name:
136
        newvalues = {"$set": {'display-name': display_name}}
137
        collection_datasets.update_one(query, newvalues)
135
    elif detect_change(current_dataset, config, compareKeys):
136
        newVal = {}
137
        for key in compareKeys:
138
            newVal[key] = config[key]
139
        collection_datasets.update_one(query, {"$set": newVal})
138 140

  
139 141

  
140 142
def update_devices_collection(config):
......
252 254
            mydb[name].drop()
253 255
            print("Dropping: " + name)
254 256

  
255
    database_record_logs.reset_ignore_set_loaded(dataset_name)
257
    database_record_logs.reset_ignore_set_loaded(dataset_name)
modules/crawler/Utilities/helpers.py
1
from typing import Dict
2

  
1 3
SKIP = "SKIP"
2 4
UNKNOWN = "UNKNOWN!"
3 5

  
......
5 7
def should_skip(device) -> bool:
6 8
    return device['x'] == SKIP or device['y'] == SKIP or device[
7 9
        'x'] == UNKNOWN or device['y'] == UNKNOWN
10

  
11

  
12
def detect_change(first: Dict[str, str], second: Dict[str, str], compareKeys: [str]) -> bool:
13
    """Detects change between two dictonaries
14

  
15
    Args:
16
        first (Dict[str, str]): First dictionary
17
        second (Dict[str, str]): Second dictionary
18
        compareKeys ([type]): Keys to handle comparison
19

  
20
    Returns:
21
        bool: Is there a change ?
22
    """
23
    for key in compareKeys:
24
        if key not in second or key not in first:
25
            return True
26
        if first[key] != second[key]:
27
            return True
28
    return False
modules/crawler/prepare_new_dataset.py
10 10
PROCESSOR_PROGRAM_PATH = "DatasetProcessing"
11 11
# Path to dataset configuration files
12 12
CONFIG_FILES_PATH = "DatasetConfigs"
13
# Default color for visualization of dataset (buble info in map)
14
DEFAULT_COLOR = "#000000"
13 15

  
14 16

  
15
def create_default_config_file(dataset_name):
17
def create_default_config_file(dataset_name: str):
16 18
    """
17 19
    Creates default config file
18 20

  
......
22 24
    with open(CONFIG_FILES_PATH + "/" + dataset_name + ".yaml", "w") as file:
23 25
        file.write("# jmeno datasetu, pod kterym bude zobrazen v aplikaci\n")
24 26
        file.write("display-name: " + dataset_name + "\n")
25
        file.write("# jednoslovný název datasetu, pod kterym bude reprezentovana v architektuře\n")
27
        file.write(
28
            "# jednoslovný název datasetu, pod kterym bude reprezentovana v architektuře\n")
29
        file.write("display-color: " + DEFAULT_COLOR + "\n")
30
        file.write(
31
            "# barva pro tento dataset v hexadecimální hodnotě (#000000)\n")
26 32
        file.write("dataset-name: " + dataset_name + "\n")
27 33
        file.write("# root slozka, ktera obsahuje odkazy na dataset\n")
28 34
        file.write("url: ZDE VLOZTE URL\n")
29
        file.write("# volitelny parameter, ktery specifikuje vzor jmrna datasetu, ktera se budou stahovat\n")
35
        file.write(
36
            "# volitelny parameter, ktery specifikuje vzor jmrna datasetu, ktera se budou stahovat\n")
30 37
        file.write("regex: ZDE VLOZTE REGEX\n")
31 38
        file.write("# volitelny parametr, ktery udava jak casto se budou hledat nove datasety, pokud prazdne, "
32 39
                   "tak defaultni hodnota (dny)\n")
......
48 55
        file.write("\n")
49 56
        file.write("def process_file(filename):\n")
50 57
        file.write("    \"\"\"\n")
51
        file.write("    Method that take path to crawled file and outputs date dictionary:\n")
52
        file.write("    Date dictionary is a dictionary where keys are dates in format YYYY-mm-dd-hh (2018-04-08-15)\n")
53
        file.write("    and value is dictionary where keys are devices (specified in configuration file)\n")
54
        file.write("    and value is CSVDataLine.csv_data_line with device,date and occurrence\n")
58
        file.write(
59
            "    Method that take path to crawled file and outputs date dictionary:\n")
60
        file.write(
61
            "    Date dictionary is a dictionary where keys are dates in format YYYY-mm-dd-hh (2018-04-08-15)\n")
62
        file.write(
63
            "    and value is dictionary where keys are devices (specified in configuration file)\n")
64
        file.write(
65
            "    and value is CSVDataLine.csv_data_line with device,date and occurrence\n")
55 66
        file.write("\n")
56 67
        file.write("    Args:\n")
57 68
        file.write("    filename: name of processed file\n")
......
63 74
        file.write("    date_dict = dict()\n")
64 75
        file.write("\n")
65 76
        file.write("    #with open(filename, \"r\") as file:\n")
66
        file.write("    print(\"You must implements process_file method first!\")\n")
77
        file.write(
78
            "    print(\"You must implements process_file method first!\")\n")
67 79
        file.write("    return None\n")
68 80

  
69 81

  
......
82 94
        file.write("\n")
83 95
        file.write("def crawl(config):\n")
84 96
        file.write("    \"\"\"\n")
85
        file.write("    Implement crawl method that downloads new data to path_for_files\n")
97
        file.write(
98
            "    Implement crawl method that downloads new data to path_for_files\n")
86 99
        file.write("    For keeping the project structure\n")
87 100
        file.write("    url , regex, and dataset_name from config\n")
88
        file.write("    You can use already implemented functions from Utilities/Crawler/BasicCrawlerFunctions.py\n")
101
        file.write(
102
            "    You can use already implemented functions from Utilities/Crawler/BasicCrawlerFunctions.py\n")
89 103
        file.write("\n")
90 104
        file.write("    Args:\n")
91 105
        file.write("        config: loaded configuration file of dataset\n")
......
93 107
        file.write("    dataset_name = config[\"dataset-name\"]\n")
94 108
        file.write("    url = config['url']\n")
95 109
        file.write("    regex = config['regex']\n")
96
        file.write("    path_for_files = CRAWLED_DATA_PATH + dataset_name + '/'\n")
110
        file.write(
111
            "    path_for_files = CRAWLED_DATA_PATH + dataset_name + '/'\n")
97 112
        file.write("    print(\"You must implements Crawl method first!\")\n")
98 113

  
99 114

  
......
123 138
    create_default_processor(dataset_name)
124 139
    create_default_config_file(dataset_name)
125 140

  
141

  
126 142
print("Zadejte jméno nového datasetu:\n")
127 143

  
128 144
dataset_name = input().upper()
website/.devcontainer/devcontainer.json
1 1
{
2
    "forwardPorts": [8000],
2
    "forwardPorts": [
3
        8000,
4
        27017
5
    ],
3 6
    "extensions": [
4 7
        "vscode-icons-team.vscode-icons",
5 8
        "felixfbecker.php-intellisense",
......
7 10
        "felixfbecker.php-debug",
8 11
        "junstyle.php-cs-fixer",
9 12
        "dbaeumer.vscode-eslint",
10
        "refgd.easy-compile"
13
        "refgd.easy-compile",
14
        "mongodb.mongodb-vscode"
11 15
    ],
12 16
    "workspaceFolder": "/var/www/symfony",
13 17
    "service": "php-fpm",
14
    "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose-dev.yml"],
15
    "mounts":["source=vscode-extensions-php,target=/var/www/symfony/.vscode-server/extensions,type=volume"]
16

  
18
    "dockerComposeFile": [
19
        "../../docker-compose.yml",
20
        "../../docker-compose-dev.yml"
21
    ],
22
    "mounts": [
23
        "source=vscode-extensions-php,target=/var/www/symfony/.vscode-server/extensions,type=volume"
24
    ]
17 25
}
website/composer.json
16 16
        "symfony/twig-pack": "*",
17 17
        "symfony/yaml": "5.0.*",
18 18
        "symfony/asset": "*",
19
		"symfony/form" : "*"
19
	"symfony/form" : "*"
20 20
    },
21 21
    "require-dev": {
22 22
        "friendsofphp/php-cs-fixer": "^2.16",
website/config/services.yaml
35 35
    App\Repository\IOpenDataManager: 
36 36
        alias: 'App\Repository\OpenDataManager'
37 37
        public: true
38
    App\Form\Type\DataSetType: ~
38
    App\Form\DataSetType: ~
website/public/css/style.css
1102 1102
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
1103 1103
  font-family: 'Be Vietnam', sans-serif;
1104 1104
  text-align: center;
1105
  color: #fff;
1106
  background-color: #0b155a !important;
1105
  color: white;
1107 1106
}
1108 1107

  
1109 1108
.leaflet-popup-content-wrapper .leaflet-popup-content strong {
......
1147 1146
.leaflet-popup-content-wrapper #count-info {
1148 1147
  margin: auto;
1149 1148
  color: #fff;
1150
  background-color: #0b155a;
1151 1149
}
1152 1150

  
1153 1151
header.map .nav-item {
......
1221 1219
  background: #0048a9;
1222 1220
  color: white;
1223 1221
}
1222

  
1223
.custom-dropdown-item {
1224
  margin: 0px !important;
1225
}
1226

  
1227
.custom-dropdown-item .disabled {
1228
  cursor: not-allowed !important;
1229
}
1230

  
1231
.custom-control-label {
1232
  font-weight: 300 !important;
1233
  margin: 0px !important;
1234
}
1235

  
1236
.custom-dropdown {
1237
  cursor: not-allowed;
1238
}
website/public/css/style.scss
1002 1002
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
1003 1003
  font-family: 'Be Vietnam', sans-serif;
1004 1004
  text-align: center;
1005
  color: #fff;
1006
  background-color: #0b155a !important;
1005
  color: white;
1007 1006
}
1008 1007

  
1009 1008
.leaflet-popup-content-wrapper {
......
1048 1047
  #count-info {
1049 1048
    margin: auto;
1050 1049
    color: #fff;
1051
    background-color: #0b155a;
1052 1050
  }
1053 1051
}
1054 1052

  
......
1125 1123
      color: white;
1126 1124
    }
1127 1125
  }
1126
}
1127
.custom-dropdown-item {
1128
  margin: 0px !important;
1129
}
1130
.custom-dropdown-item .disabled {
1131
  cursor: not-allowed !important;
1132
}
1133
.custom-control-label {
1134
  font-weight: 300 !important;
1135
  margin: 0px !important;
1136
}
1137
.custom-dropdown {
1138
  cursor: not-allowed;
1128 1139
}
website/public/js/cache-handler.js
1
const cache = {}
2
export const updateCache = (key, value, cache_ = cache) => {
3
  if (typeof value === 'object') {
4
    if (!(key in cache_)) {
5
      cache_[key] = {}
6
    }
7
    const keys = Object.keys(value)
8
    if (!keys.length) {
9
      return undefined
10
    }
11
    keys.forEach((key_) => {
12
      cache_[key][key_] = updateCache(key_, value[key_], cache_[key])
13
    })
14
  } else if (Array.isArray(value)) {
15
    cache_[key] = value.map((item) => {
16
      updateCache()
17
    })
18
  } else {
19
    cache_[key] = value
20
  }
21
  return cache_
22
}
23
export const getCachedData = (key) => {
24
  return cache[key]
25
}
26
export default cache
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
}
website/src/Controller/HeatmapController.php
2 2

  
3 3
namespace App\Controller;
4 4

  
5
use App\Utils\Utils;
5 6
use App\Entity\DataSet;
6
use App\Form\DataSetType;
7
use App\Form\DatasetFormBuilder;
7 8
use App\Repository\IOpenDataManager;
8 9
use Symfony\Component\HttpFoundation\Request;
9 10
use Symfony\Component\Routing\Annotation\Route;
......
19 20
     */
20 21
    public function index(Request $request, IOpenDataManager $manager) {
21 22
        $dataSet = new DataSet();
23
        $datasetFormBuilder = new DatasetFormBuilder($manager);
24
        $formBuilder = $datasetFormBuilder->getBuilder();
22 25

  
23
        $form = $this->createForm(DataSetType::class, $dataSet);
24
        $form->handleRequest($request);
25

  
26
        $form = $formBuilder->getForm();
27
        $form->submit($request->query->all());
26 28
        $isSubmitted = $form->isSubmitted();
27 29
        if ($isSubmitted) {
28 30
            $dataSet = $form->getData();
29
            // Chceck whether the data is valid i.e. such a collection exists
30
            if (false == $manager->isCollectionAvailable($dataSet->getType(), $dataSet->getDate())) {
31
            //check availability
32
            $notExist = false;
33
            if ($dataSet->getType() && count($dataSet->getType()) > 0) {
34
                foreach ($dataSet->getType() as $value) {
35
                    if (!$manager->isCollectionAvailable($value, $dataSet->getDate())) {
36
                        $notExist = true;
37
                        break;
38
                    }
39
                }
40
            } else {
41
                $notExist = true;
42
            }
43
            if ($notExist) {
31 44
                // Not? Populate form with empty data
32
                $dataSet = new DataSet();
45
                $formBuilder = $datasetFormBuilder->getBuilder();
46
                $form = $formBuilder->getForm();
33 47
            }
34
            $form = $this->createForm(DataSetType::class, $dataSet);
35 48
        }
49
        //ziskej barvy o datasetech a namapuj je na jejich jmena
36 50

  
37 51
        return $this->render(
38 52
            'heatmap.html.twig',
......
40 54
                'form' => $form->createView(),
41 55
                'submitted' => $isSubmitted,
42 56
                'data_to_display' => $dataSet,
57
                'dataset_colors' => Utils::prepareDatasetsColors($manager->getAvailableCollections()),
43 58
            ]
44 59
        );
45 60
    }
website/src/Form/DatasetFormBuilder.php
1
<?php
2

  
3
namespace App\Form;
4

  
5
use App\Utils\Utils;
6
use App\Entity\DataSet;
7
use Symfony\Component\Form\Forms;
8
use App\Repository\IOpenDataManager;
9
use Symfony\Component\Form\Extension\Core\Type\FormType;
10
use Symfony\Component\Form\Extension\Core\Type\TextType;
11
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
12
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
13

  
14
class DatasetFormBuilder {
15
    /**
16
     * @var IOpenDataManager autowired connection to database
17
     */
18
    private $manager;
19

  
20
    /**
21
     * @param IOpenDataManager autowired connection to database
22
     */
23
    public function __construct(IOpenDataManager $manager) {
24
        $this->manager = $manager;
25
    }
26

  
27
    public function getBuilder() {
28
        $dataSet = new DataSet();
29

  
30
        return Forms::createFormFactoryBuilder()->getFormFactory()->createNamedBuilder('', FormType::class, $dataSet)
31
            ->add('date', TextType::class)
32
            ->add('time', ChoiceType::class, [
33
                'choices' => [
34
                    '0:00-1:00' => 0,
35
                    '1:00-2:00' => 1,
36
                    '2:00-3:00' => 2,
37
                    '3:00-4:00' => 3,
38
                    '4:00-5:00' => 4,
39
                    '5:00-6:00' => 5,
40
                    '6:00-7:00' => 6,
41
                    '7:00-8:00' => 7,
42
                    '8:00-9:00' => 8,
43
                    '9:00-10:00' => 9,
44
                    '10:00-11:00' => 10,
45
                    '11:00-12:00' => 11,
46
                    '12:00-13:00' => 12,
47
                    '13:00-14:00' => 13,
48
                    '14:00-15:00' => 14,
49
                    '15:00-16:00' => 15,
50
                    '16:00-17:00' => 16,
51
                    '17:00-18:00' => 17,
52
                    '18:00-19:00' => 18,
53
                    '19:00-20:00' => 19,
54
                    '20:00-21:00' => 20,
55
                    '21:00-22:00' => 21,
56
                    '22:00-23:00' => 22,
57
                    '23:00-0:00' => 23,
58
                ],
59
            ])->add('type', ChoiceType::class, [
60
                'choices' => Utils::prepareDatasetsNames($this->manager->getAvailableCollections()),
61
                'multiple' => true,
62
                'expanded' => true,
63
            ])
64
            ->add('submit', SubmitType::class);
65
    }
66
}
website/src/Repository/OpenDataManager.php
46 46
    }
47 47

  
48 48
    public function getAvailableCollections() {
49
        $openData = $this->manager->executeQuery('open-data-db.DATASETS', new Query([], ['projection' => ['key-name' => 1, 'display-name' => 1, '_id' => 0]]));
49
        $openData = $this->manager->executeQuery('open-data-db.DATASETS', new Query([], ['projection' => ['key-name' => 1, 'display-name' => 1, 'display-color' => 1, '_id' => 0]]));
50 50

  
51 51
        // Converts result to php array
52 52
        $openData->setTypeMap([
website/src/Utils/Utils.php
17 17
    public static function prepareDatasetsNames($datasets) {
18 18
        $names = [];
19 19

  
20
        $index = 0;
21 20
        foreach ($datasets as $key => $value) {
22 21
            if (false == array_key_exists($value['key-name'], $names)) {
23 22
                $names[$value['display-name']] = $value['key-name'];
......
26 25

  
27 26
        return $names;
28 27
    }
28

  
29
    public static function prepareDatasetsColors($datasets) {
30
        $colors = [];
31
        foreach ($datasets as $key => $value) {
32
            if (false == array_key_exists($value['key-name'], $colors)) {
33
                $colors[$value['key-name']] = $value['display-color'];
34
            }
35
        }
36

  
37
        return $colors;
38
    }
29 39
}
website/symfony.code-workspace
15 15
		"eslint.codeActionsOnSave.mode": "all",
16 16
		"editor.codeActionsOnSave": {
17 17
			"source.fixAll": true
18
		}
18
		},
19 19
	},
20 20
	"extensions": {
21 21
		"recommendations": [
website/templates/base.html.twig
22 22
    <body{% block bodyClass %}{% endblock %}>
23 23
        {% block body %}{% endblock %}
24 24
        {% block javascripts %}{% endblock %}
25
        {% block style %}{% endblock %}
25 26
    </body>
26 27
</html>
website/templates/heatmap.html.twig
51 51
                  'id' : 'time',
52 52
                  'attr' : {
53 53
                    'class' : 'custom-select'
54
                  }
54
                  },
55 55
                })
56 56
              }}             
57 57
            </li>
58 58

  
59 59
            <li class="nav-item">
60
              {{ form_label(form.type, 'Vyberte datovou sadu') }}
61
              {{
62
                form_widget(form.type, {
63
                  'id' : 'type',
64
                  'attr' : {
65
                    'class' : 'custom-select'
66
                  }
67
                })
68
              }}  
60
              <div class="dropdown">
61
              {# {{ form_label(form.type, 'Vyberte datovou sadu') }} #}
62
                <button class="btn dropdown-toggle text-white font-weight-bold" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
63
                  Vyberte datovou sadu
64
              </button>
65
              <div id="dataset-dropdown" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
66
              {% for oneType in form.type.children %}
67
              <label class="dropdown-item custom-dropdown-item">
68
              <div class="custom-control custom-checkbox custom-checkbox-{{oneType.vars.value}}">
69
              {{ form_widget(oneType, {
70
                attr: {'class': "custom-control-input custom-control-input-#{oneType.vars.value}"}
71
              }) }}
72
              {{ form_label(oneType, null, {
73
                'label_attr': {'class': 'custom-control-label'}
74
                }) }}
75
              </div>
76
              </label>
77
              {% endfor %}
78
              </div>
79
              </div>  
69 80
            </li>
70 81

  
71 82
            <li class="nav-item">
72 83
              {{
73 84
                form_widget(form.submit,
74 85
                {
86
                  'name' : '',
75 87
                  'label' : 'Potvrdit výběr',
76 88
                  'type' : 'submit',
77 89
                  'id' : 'submit-btn',
......
171 183
    <script>
172 184
        initDatepicker("{{ path('dates') }}");
173 185
        initLocationsMenu();
174
        initMap();
175
        checkDataSetsAvailability("{{ path('available') }}")
176
        {% if submitted %}
177
        loadCurrentTimeHeatmap("{{ path('opendata') }}", "{{ path('positions') }}");
178
        {% endif %}
179

  
186
        initMap();        
187
        $( document ).ready(function() {
188
          onDocumentReady();
189
          checkDataSetsAvailability("{{ path('available') }}")
190
          {% if submitted %}
191
          loadCurrentTimeHeatmap("{{ path('opendata') }}", "{{ path('positions') }}");
192
          {% endif %}
193
        });
180 194
        $(window).resize(function() {
181 195
          initLocationsMenu();
182 196
        });
183 197
  </script>
184 198

  
199
{% endblock %}
200

  
201
{% block style %}
202
  <style>
203
    {% for key, item in dataset_colors %}
204
      .custom-control-input-{{key}}:focus~.custom-control-label::before {
205
          border-color: {{item}} !important;
206
          box-shadow: 0 0 0 0.2rem rgba(192,192,192, 0.4) !important;
207
      }
208

  
209
      .custom-control-input-{{key}}:checked~.custom-control-label::before {
210
          border-color: {{item}} !important;
211
          background-color: {{item}} !important;
212
      }
213

  
214
      .custom-control-input-{{key}}:focus:not(:checked)~.custom-control-label::before {
215
          border-color: {{item}} !important;
216
      }
217

  
218
      .custom-control-input-{{key}}:not(:disabled):active~.custom-control-label::before {
219
          background-color: {{item}} !important;
220
          border-color: {{item}} !important;
221
      }
222

  
223
      .popup-{{key}} > .leaflet-popup-content-wrapper, .popup-{{key}} .leaflet-popup-tip{
224
        background-color: {{item}} !important;
225
      }
226
    {% endfor %}
227
  </style>
185 228
{% endblock %}
website/templates/index.html.twig
45 45
          </li>
46 46
          <li class="nav-item button">
47 47
            <h2>
48
              <a href="/heatmap/?data_set[date]=2019-04-11&amp;data_set[time]=9&amp;data_set[type]=JIS" class="nav-link">Zobrazit heatmapu</a>
48
              <a href="/heatmap/?date=2019-04-11&amp;time=9&amp;type[]=JIS" class="nav-link">Zobrazit heatmapu</a>
49 49
            </h2>
50 50
          </li>
51 51
        </ul>
......
62 62
            <h3>Život na ZČU během dne</h3>
63 63
            <hr>
64 64
            <p>Zobrazte si heatmapu života univerzity hodinu po hodině během jednoho dne. Od ranního probouzení, kdy studenti a&nbsp;zaměstnanci přicházejí do laboratoří, přes polední špičku v&nbsp;menze až do samotného večera, kdy končí poslední z&nbsp;přednášek a&nbsp;cvičení.</p>
65
            <a href="/heatmap/?data_set[date]=2019-04-11&amp;data_set[time]=9&amp;data_set[type]=JIS" class="btn btn-primary" role="button">Vyzkoušet heatmapu</a>
65
            <a href="/heatmap/?date=2019-04-11&amp;time=9&amp;type[]=JIS" class="btn btn-primary" role="button">Vyzkoušet heatmapu</a>
66 66
          </div>
67 67
        </div>
68 68

  
website/tests/Controller/HeatmapControllerTest.php
9 9
    public function testPageLoad() {
10 10
        $client = static::createClient();
11 11
        $client->request('GET', '/heatmap');
12

  
12 13
        $this->assertTrue($client->getResponse()->isSuccessful());
13 14
    }
14 15

  
15 16
    public function testPageLoadWithData() {
16 17
        $client = static::createClient();
17
        $client->request('GET', '/heatmap?data_set[date]=2019-04-11&data_set[time]=12&data_set[type]=JIS');
18
        $client->request('GET', '/heatmap?date=2019-04-11&time=12&type[]=JIS');
19
        
18 20
        $this->assertTrue($client->getResponse()->isSuccessful());
19 21
    }
20 22

  
21 23
    public function testPageLoadWithInvalidData() {
22 24
        $client = static::createClient();
23
        $client->request('GET', '/heatmap?data_set[date]=2019-04-11&data_set[time]=12&data_set[type]=NIC');
25
        $client->request('GET', '/heatmap?date=2019-04-11&time=12&type[]=NIC');
24 26
        $this->assertTrue($client->getResponse()->isSuccessful());
25 27

  
26
        $client->request('GET', '/heatmap?data_set[date]=2019-04-11&data_set[time]=25&data_set[type]=KOLOBEZKY');
28
        $client->request('GET', '/heatmap?date=2019-04-11&time=25&type[]=KOLOBEZKY');
27 29
        $this->assertTrue($client->getResponse()->isSuccessful());
28 30

  
29
        $client->request('GET', '/heatmap?data_set[date]=11-04-2019&data_set[time]=12&data_set[type]=KOLOBEZKY');
31
        $client->request('GET', '/heatmap?date=11-04-2019&time=12&type[]=KOLOBEZKY');
30 32
        $this->assertTrue($client->getResponse()->isSuccessful());
31 33
    }
32 34

  
......
39 41

  
40 42
        $crawler = $client->submitForm('Potvrdit výběr', 
41 43
        [
42
            'data_set[date]' => '2019-04-11',
43
            'data_set[time]' => '0',
44
            'data_set[type]' => 'JIS'
44
            'date' => '2019-04-11',
45
            'time' => '0',
46
            'type[1]' => 'JIS'
45 47
        ]);
46 48
        $this->assertTrue($client->getResponse()->isSuccessful());
47 49
    }
......
52 54

  
53 55
        $crawler = $client->submitForm('Potvrdit výběr', 
54 56
        [
55
            'data_set[date]' => '11-04-2019',
56
            'data_set[time]' => '0',
57
            'data_set[type]' => 'JIS'
57
            'date' => '11-04-2019',
58
            'time' => '0',
59
            'type[1]' => 'JIS'
58 60
        ]);
59 61
        $this->assertTrue($client->getResponse()->isSuccessful());
60 62
    }

Také k dispozici: Unified diff