Revize 2f227a6c
Přidáno uživatelem Tomáš Ballák před více než 4 roky(ů)
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&data_set[time]=9&data_set[type]=JIS" class="nav-link">Zobrazit heatmapu</a>
|
|
48 |
<a href="/heatmap/?date=2019-04-11&time=9&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 zaměstnanci přicházejí do laboratoří, přes polední špičku v menze až do samotného večera, kdy končí poslední z přednášek a cvičení.</p> |
65 |
<a href="/heatmap/?data_set[date]=2019-04-11&data_set[time]=9&data_set[type]=JIS" class="btn btn-primary" role="button">Vyzkoušet heatmapu</a>
|
|
65 |
<a href="/heatmap/?date=2019-04-11&time=9&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
Feature Re #8156 show all datasets