Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94624d5b2c | ||
|
|
71c728d56c | ||
|
|
216d30329f | ||
|
|
3f8311d708 | ||
|
|
7f8c1eb291 | ||
|
|
d57a4c63e0 | ||
|
|
ed8e1773c4 | ||
|
|
2963bfe739 | ||
|
|
7232b322ab | ||
|
|
e6819b6dd3 | ||
|
|
01a4fdf5e5 | ||
|
|
1cc3294ee7 | ||
|
|
ad0b8d078f | ||
|
|
899817a853 | ||
|
|
c626e36bbb | ||
|
|
8d8074977b | ||
|
|
8b3c95bdc7 | ||
|
|
b2535ad78a | ||
|
|
886bd02f88 | ||
|
|
7143c7ec63 | ||
|
|
fb7c0e7a5c | ||
|
|
8136273b33 | ||
|
|
3c6a7a3c5f | ||
|
|
01f3c7c8cf | ||
|
|
66c871fbc2 | ||
|
|
807b4d024f | ||
|
|
430b32d0c6 | ||
|
|
11eee64895
|
||
|
|
cc24f96395 | ||
|
|
c3e5a6db1c | ||
|
|
398204c5d5 | ||
|
|
aaa5a1daba | ||
|
|
c112a25409
|
||
|
|
35cab17c21
|
||
|
|
f9c1829a00
|
||
|
|
e03850ce05
|
||
|
|
55ec226407 | ||
|
|
6ef04fcd4e
|
||
|
|
3045d8acbb
|
||
|
|
19a1b79b0a | ||
|
|
d432a5b584 | ||
|
|
8b9345eb36 | ||
|
|
e1eb0077b1 | ||
|
|
745bda11c0 | ||
|
|
0bb31cbe14 | ||
|
|
ef8ad69506 | ||
|
|
f10ad37a74 | ||
|
|
70b60d2fb8
|
||
|
|
7e68580db6
|
||
|
|
8218883faf | ||
|
|
145cb5f637 | ||
|
|
6d3f74f911 | ||
|
|
3d663fb5d6 | ||
|
|
759f654a0d | ||
|
|
c09cce3c97 | ||
|
|
276e331a47
|
||
|
|
62f4fd7b84 | ||
|
|
b22f13887f | ||
|
|
2a3f2212a7 | ||
|
|
98df08e5fa | ||
|
|
8f0cfd4489 | ||
|
|
d42280eb10 | ||
|
|
0739c65961 | ||
|
|
3122696364 | ||
|
|
462f6cde80 | ||
|
|
3f4fa83d87 | ||
|
|
ea599809b8 | ||
|
|
f0b0de405c | ||
|
|
ffc4aaf110 | ||
|
|
58cdc1be8d | ||
|
|
4039202268
|
||
|
|
4677cc5229 | ||
|
|
8800724aa9
|
||
|
|
08c38c6374 | ||
|
|
854775011c | ||
|
|
e33d8c02e9 | ||
|
|
3d52c967b6 | ||
|
|
5c8f3d6fb9 | ||
|
|
b9927f79fc | ||
|
|
56f3a9b380 | ||
|
|
a0d41c388b | ||
|
|
092cc63148 | ||
|
|
95553296bd | ||
|
|
9330227879 | ||
|
|
faaff33841 | ||
|
|
508a6899d8 | ||
|
|
be0f7b64b8 | ||
|
|
15e3248ee1 | ||
|
|
04a897c958 | ||
|
|
845d77ebda | ||
|
|
8bc3fda49b | ||
|
|
d78817180b | ||
|
|
0f810bd2ba | ||
|
|
9d1fc8b60c
|
||
|
|
dd46fb3d32
|
||
|
|
1a40e3a18d | ||
|
|
1aa5d3b43e
|
||
|
|
a418d5cf66 | ||
|
|
5cae286a5b
|
||
|
|
c5d2591371 | ||
|
|
cf59f937c9
|
||
|
|
257f59c96e | ||
|
|
c2f5806394
|
||
|
|
ac0b71fe09 | ||
|
|
19f7b50002
|
||
|
|
6342bb1e79
|
||
|
|
3abdb6910e
|
||
|
|
7d2588f7cd | ||
|
|
be40acdb2d
|
||
|
|
a2f2080959
|
||
|
|
cc49c56075 | ||
|
|
69329b9f81
|
||
|
|
20914b466e
|
||
|
|
3630637ec1
|
||
|
|
42224b074e
|
||
|
|
0e8ed14305
|
||
|
|
d3a12fb6f1 | ||
|
|
3f9d104adc
|
||
|
|
9e5d20796c
|
||
|
|
7433813337 | ||
|
|
42854fe975
|
||
|
|
71681f1e55 | ||
|
|
1cf0ab44fd
|
||
|
|
fc9eb183f1 | ||
|
|
d431a693fc
|
||
|
|
ace490b7e9 | ||
|
|
e3771d36ce
|
||
|
|
6e03476173 | ||
|
|
f3ec71d6dd
|
||
|
|
c2f0a1dd51
|
||
|
|
a417fbd760 | ||
|
|
a9cda56586
|
||
|
|
02fcbd2f9c
|
||
|
|
2f665381c3
|
||
|
|
573440868e
|
||
|
|
79490a0616
|
||
|
|
ed343f2359
|
||
|
|
563f900423
|
||
|
|
24a10c534e
|
||
|
|
0cbb7ba145
|
||
|
|
5dd2ce0d5a
|
||
|
|
e5fb725c69
|
||
|
|
fc40c18679
|
||
|
|
4fabc39d44
|
||
|
|
2120fbebbc
|
||
|
|
b9f0529dce
|
||
|
|
a3ca7638c7
|
||
|
|
8538d0a119
|
||
|
|
0cc63ef936 | ||
|
|
10d239bfb4
|
||
|
|
e2493e1b7e | ||
|
|
edd8c0f8a4
|
||
|
|
01f0f7d4bc | ||
|
|
c317a0abf4
|
||
|
|
9c4b7448a9
|
||
|
|
60e20bd92b
|
||
|
|
2c4d809d49
|
||
|
|
bf27f15709 | ||
|
|
db3094eb62
|
||
|
|
77c71ab2da | ||
|
|
8327cd975a
|
||
|
|
cc8148fcf9 | ||
|
|
2a7b5fb992
|
||
|
|
a4c77c3d32 | ||
|
|
317329ae84
|
||
|
|
234cb7f086 | ||
|
|
66c709f00d
|
||
|
|
2737c7d6d6
|
||
|
|
2ccee26424 | ||
|
|
8cbf611189
|
||
|
|
2ac9794e3f | ||
|
|
9e5f7405d1
|
||
|
|
af98d71924
|
||
|
|
39d691fd7f
|
||
|
|
405488cf4e
|
||
|
|
f2736d4d6e | ||
|
|
88b9821c61
|
||
|
|
da2c8d96a4 | ||
|
|
cd581c6fcd
|
||
|
|
f7b268e10f | ||
|
|
a8b1ab3057
|
||
|
|
8403c21ec0
|
||
|
|
0ba1cda884
|
||
|
|
b2e09c394b
|
||
|
|
0bcb0187c1
|
||
|
|
f3d1608164
|
||
|
|
b254603fb1
|
||
|
|
776561a41c | ||
|
|
27fc687043
|
||
|
|
45869321ac
|
||
|
|
b58137acc9 | ||
|
|
dd6bb5ef9a
|
||
|
|
37094e552b
|
||
|
|
32392590bd
|
||
|
|
38885849de
|
||
|
|
ef013b8b9e
|
||
|
|
ab23b37b97
|
||
|
|
7e3315fc88
|
||
|
|
6cafac1b65
|
||
|
|
631582a912 | ||
|
|
6db6208896
|
||
|
|
e17cf68007
|
||
|
|
091afb0aab | ||
|
|
954f111254
|
||
|
|
de1d7338a4
|
||
|
|
7539f637b0
|
||
|
|
67534a22f5
|
||
|
|
43aef22c4e | ||
|
|
3123643bb7
|
||
|
|
b736ce8970
|
||
|
|
4e48e6ae73
|
||
|
|
028d8a22a2
|
||
|
|
a73e60ae5a | ||
|
|
5383da81b3
|
||
|
|
e8adc9f730 | ||
|
|
149003250c
|
||
|
|
171b4278a6 | ||
|
|
091018d22f
|
||
|
|
34f0c92eb5 | ||
|
|
8ac5d5c6e2
|
||
|
|
e883d184b7
|
||
|
|
71b1e28aa9 | ||
|
|
3555be9b63
|
||
|
|
d03a3934c6 | ||
|
|
bd5b49b09b
|
||
|
|
4333240acc
|
||
|
|
4099f1d962
|
||
|
|
2fac828098
|
||
|
|
f3225ae275
|
||
|
|
48eba75cf4
|
||
|
|
a97a6f8977 | ||
|
|
2a3f891aa0
|
||
|
|
38a427416c | ||
|
|
2f14e1f4c3
|
||
|
|
6098b7bb8d | ||
|
|
596b29f841
|
||
|
|
34a4026ddc | ||
|
|
a55ece7dac
|
||
|
|
efdd5c54be
|
||
|
|
825e0744ec
|
||
|
|
18bf9aaade
|
||
|
|
5e645a1313 | ||
|
|
9c9edcbbd1
|
||
|
|
de009637de | ||
|
|
6fce002458
|
||
|
|
1287fa0bc9
|
||
|
|
bf8e5b5f4d
|
||
|
|
c8a49396c2
|
||
|
|
c2323c4c89
|
||
|
|
60cbe64718
|
||
|
|
c48bbdc3c2
|
||
|
|
ee6aa59253 | ||
|
|
e1df28123f
|
||
|
|
b4769b62dd | ||
|
|
bee991e300 | ||
|
|
ddc56426c2 | ||
|
|
c06dd1e1d2 | ||
|
|
fd657b4244 | ||
|
|
532f099ec9 | ||
|
|
ec969167e4 | ||
|
|
e4a51c139f | ||
|
|
27115e44b6 | ||
|
|
45904a98c2 | ||
|
|
b1ae7391e7 | ||
|
|
8987b563dd | ||
|
|
70d8807f38 | ||
|
|
910fdddf34 | ||
|
|
87c4c06aa6 | ||
|
|
ad24f8ac28 | ||
|
|
e2067627c4 | ||
|
|
a8419535e1 | ||
|
|
725d14400b | ||
|
|
b75eb16b3d | ||
|
|
f8a570d0d5 | ||
|
|
e7ee7bbe8e | ||
|
|
2f4f66cc94 | ||
|
|
4afbc046ff | ||
|
|
79bd5b98d5 | ||
|
|
c1cdeba1d3 | ||
|
|
255168ffb6 | ||
|
|
ca0b8da733 | ||
|
|
34206cac12 | ||
|
|
47a2f15fc5 | ||
|
|
17658f70ba | ||
|
|
500ec958cb | ||
|
|
d2fa2050ac | ||
|
|
e8be433ee6 | ||
|
|
506072017c | ||
|
|
4561c825a4 | ||
|
|
693bddbcf0 | ||
|
|
eae7cefae1 | ||
|
|
212728e138 | ||
|
|
0d627f297e | ||
|
|
f07ae979a2 | ||
|
|
8bdbbafcff | ||
|
|
3856ccb576 | ||
|
|
9f839d3f5d | ||
|
|
721b611cdb | ||
|
|
70fb30f673 | ||
|
|
e200dab2a4 | ||
|
|
e8c246f002 | ||
|
|
251bf1be3d | ||
|
|
0be20a6097 | ||
|
|
d5d3563ddc | ||
|
|
19aa4098e5 | ||
|
|
394c8b8830 | ||
|
|
dc4ba4b5a4 | ||
|
|
eda4e00378 | ||
|
|
dfc74375d7 | ||
|
|
dc1aedd28f | ||
|
|
787427b211 | ||
|
|
05b1a84168 | ||
|
|
e2a0da3dd2 | ||
|
|
e751fdaa3e | ||
|
|
675d7e60d0 | ||
|
|
0d097aacb5 | ||
|
|
98cd15e91f | ||
|
|
4f680db8f3 | ||
|
|
ea98af0681 | ||
|
|
05934d6245 | ||
|
|
3ab8ac5fe6 | ||
|
|
94e60e4ac5 | ||
|
|
9df5028c20 | ||
|
|
49bd03f303 | ||
|
|
7210e12656 | ||
|
|
8320b94210 | ||
|
|
28aa762153 | ||
|
|
33eb154112 | ||
|
|
a2ca7888f4 | ||
|
|
e0395132d6 | ||
|
|
aeaf0fe8d3 | ||
|
|
d96ac938f8 | ||
|
|
cb5229a5e8 | ||
|
|
5a4fe48a9e | ||
|
|
02d6298d95 | ||
|
|
a56ce849c0 | ||
|
|
87d476065d | ||
|
|
df60efbb8c | ||
|
|
6338e35cfb | ||
|
|
1cfbef418e | ||
|
|
a8777b897b | ||
|
|
285f2dd0ac | ||
|
|
10b7766458 | ||
|
|
9697163457 | ||
|
|
d1c9e221f7 | ||
|
|
2d2e218002 | ||
|
|
7ba11f34cc | ||
|
|
99c72d99be | ||
|
|
295570a28d | ||
|
|
e612d0d93f | ||
|
|
a2d2b94cc6 | ||
|
|
80b850fdd7 | ||
|
|
0bf6e900fe | ||
|
|
bd46809d3a | ||
|
|
821a9d80c0 | ||
|
|
eafa98571e | ||
|
|
316e39f2f2 | ||
|
|
5ff720e3c6 | ||
|
|
a9eec05288 | ||
|
|
d6750777c2 | ||
|
|
1a4d928143 | ||
|
|
b35668c2fb | ||
|
|
5814620b4f | ||
|
|
64b6197bd4 | ||
|
|
1be51b3e07 | ||
|
|
43775c8f4f | ||
|
|
95eb956403 | ||
|
|
ce96e15911 | ||
|
|
3726573f4c | ||
|
|
a8913b0d44 | ||
|
|
e014bf9dc1 | ||
|
|
50c7e3972c | ||
|
|
efccb2dcc5 | ||
|
|
a38268c751 | ||
|
|
15b5869d5a | ||
|
|
bd2b28ef17 | ||
|
|
6d431717f8 | ||
|
|
4ac18dfc84 | ||
|
|
afbd0b4e20 | ||
|
|
062718dd75 | ||
|
|
20102f0577 | ||
|
|
a69ba3a99b | ||
|
|
66b21ec710 | ||
|
|
28ed02091a | ||
|
|
334df3462b | ||
|
|
01db72796b | ||
|
|
c50c5ed422 | ||
|
|
142c91f36b | ||
|
|
1269c876e9 | ||
|
|
44e441fac0 | ||
|
|
f85340cd00 | ||
|
|
ba83f73c31 | ||
|
|
86f05f7738 | ||
|
|
61ce915ec0 | ||
|
|
2aa3cdc7f3 | ||
|
|
5760e31e29 | ||
|
|
14fd723f26 | ||
|
|
a6d5214166 | ||
|
|
d1d9dede9c | ||
|
|
171064816c | ||
|
|
f8bb7bd092 | ||
|
|
466f065718 | ||
|
|
6d68f247e6 | ||
|
|
e4c7aadc64 | ||
|
|
25262fb5fe | ||
|
|
8e0c1ce08b | ||
|
|
9e196ebf33 | ||
|
|
013668e3d6 | ||
|
|
dbd16d6cbe | ||
|
|
6a191d5c28 | ||
|
|
39e49724da | ||
|
|
6c8a3c6a90 | ||
|
|
7159867db0 | ||
|
|
4d7c86fd1b | ||
|
|
9ca97dccb9 | ||
|
|
2ef67d53f4 | ||
|
|
06a0722899 | ||
|
|
4a9cf8a217 | ||
|
|
b3caf1fb38 | ||
|
|
08d4aec48e | ||
|
|
9c380076fa | ||
|
|
95508ad284 | ||
|
|
7690714b26 | ||
|
|
f732dad1b1 | ||
|
|
49b6f28a9d | ||
|
|
33ec9d3a9c | ||
|
|
6fb122e842 | ||
|
|
90ccdf6de7 | ||
|
|
a869a55b7e | ||
|
|
420ed400a6 | ||
|
|
d6f34b916e | ||
|
|
98fc735622 | ||
|
|
81a3079a1a | ||
|
|
a0d1e8778c | ||
|
|
2fc0ab557f | ||
|
|
be8f0a1486 | ||
|
|
ff201c2e8e | ||
|
|
805d91c0c0 | ||
|
|
ec7e806277 | ||
|
|
947ce6b183 | ||
|
|
6fc83c9459 | ||
|
|
5237c3f454 | ||
|
|
5a1c384c17 | ||
|
|
3fb538469b | ||
|
|
04d10663dd | ||
|
|
f11cba4f10 | ||
|
|
cfb0513825 | ||
|
|
ed0e287c94 | ||
|
|
ad818af543 | ||
|
|
2725754504 | ||
|
|
dc072bd3c0 | ||
|
|
a70d59ed95 | ||
|
|
94cab9f433 | ||
|
|
4b0057e3ba | ||
|
|
5a450c46db | ||
|
|
4af6f07ce1 | ||
|
|
5e28fcb64d | ||
|
|
16bcbaecbe | ||
|
|
fbab712496 | ||
|
|
46ef7f3b3b | ||
|
|
5dd877d075 | ||
|
|
88e8c5c027 | ||
|
|
3fd37b0a78 | ||
|
|
92fd282184 | ||
|
|
55a1cd24d6 | ||
|
|
bb29bfdfe1 | ||
|
|
1179914f42 | ||
|
|
85773d47a0 | ||
|
|
199374a851 | ||
|
|
138ff3fe6a | ||
|
|
1915b252c6 | ||
|
|
2f2a7968a0 | ||
|
|
edc2e4111a | ||
|
|
d93a758cd2 | ||
|
|
e8f9d74283 | ||
|
|
00dc78b50c | ||
|
|
fe814908c7 | ||
|
|
ecaf8ca9d4 | ||
|
|
ed9621d581 | ||
|
|
b6f8735da8 | ||
|
|
6a9f7eddc1 | ||
|
|
af52ac3787 | ||
|
|
0481274335 | ||
|
|
618f2a4d48 | ||
|
|
e817a34a3a | ||
|
|
4dde24b70a | ||
|
|
2d91f94f0c | ||
|
|
dd2905286b | ||
|
|
12d24bfddf | ||
|
|
338537c774 | ||
|
|
b2ee25fcc5 | ||
|
|
af2375133a | ||
|
|
83a027c1bb | ||
|
|
4b2fc23129 | ||
|
|
c0c44bcbae | ||
|
|
e0273a4c41 | ||
|
|
b2c1749695 | ||
|
|
425d0d891a | ||
|
|
0d3203c10e | ||
|
|
cf63287ecc | ||
|
|
8b9dd0bc32 | ||
|
|
6acb19ba36 | ||
|
|
7f8e0fefa9 | ||
|
|
87ef0d1918 | ||
|
|
41b989d6e3 | ||
|
|
aa68e748dd | ||
|
|
637ed5d0ac | ||
|
|
b4de510a65 | ||
|
|
c51c023357 | ||
|
|
150bc5be20 | ||
|
|
9085793c9f | ||
|
|
a82b27cb90 | ||
|
|
f7d37b8513 | ||
|
|
b823af1f7d | ||
|
|
601598367e |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
APP_NAME=MapGuesser
|
||||
DEV=1
|
||||
DB_HOST=mariadb
|
||||
DB_USER=mapguesser
|
||||
@@ -5,3 +6,17 @@ DB_PASSWORD=mapguesser
|
||||
DB_NAME=mapguesser
|
||||
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
|
||||
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
|
||||
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
|
||||
LEAFLET_TILESERVER_SUBDOMAINS=list_of_subdomains_for_the_tileserver_without_separators
|
||||
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
|
||||
STATIC_ROOT=/static
|
||||
MAIL_FROM=mapguesser@mapguesser-dev.ch
|
||||
MAIL_HOST=mail
|
||||
MAIL_PORT=2500
|
||||
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
|
||||
GOOGLE_ANALITICS_ID=your_google_analytics_id
|
||||
MULTI_INTERNAL_HOST=multi
|
||||
MULTI_INTERNAL_PORT=5000
|
||||
MULTI_WS_URL=mapguesser-dev.ch:8090
|
||||
MULTI_WS_PORT=8090
|
||||
@@ -1,3 +1,4 @@
|
||||
.env
|
||||
installed
|
||||
vendor
|
||||
node_modules
|
||||
@@ -9,6 +9,14 @@
|
||||
"pathMappings": {
|
||||
"/var/www/mapguesser": "${workspaceRoot}",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen for NodeJS Inspector in Docker",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/var/www/mapguesser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,103 @@
|
||||
# MapGuesser
|
||||
|
||||
This is the MapGuesser Application project.
|
||||
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
|
||||
|
||||
License: GNU AGPL 3.0
|
||||
## Installation
|
||||
|
||||
### Clone the Git repository
|
||||
|
||||
The first step is obviously cloning the repository to your machine:
|
||||
|
||||
```
|
||||
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
|
||||
```
|
||||
|
||||
All the commands listed here should be executed from the repository root.
|
||||
|
||||
### Setup Docker stack (recommended)
|
||||
|
||||
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
|
||||
|
||||
All you have to do is executing the following command:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Attach shell to the container of `mapguesser_app`:
|
||||
|
||||
```
|
||||
docker exec -it mapguesser_app_1 bash
|
||||
```
|
||||
|
||||
All of the following commands should be executed there.
|
||||
|
||||
### Manual setup (alternative)
|
||||
|
||||
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
|
||||
|
||||
### Initialize project
|
||||
|
||||
This command installes all of the Composer requirements and creates a copy of the example `.env` file.
|
||||
|
||||
```
|
||||
composer create-project
|
||||
```
|
||||
|
||||
### Set environment variables
|
||||
|
||||
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
|
||||
|
||||
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
|
||||
|
||||
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
|
||||
|
||||
#### API keys
|
||||
|
||||
**You should set the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
|
||||
|
||||
Required Google APIs:
|
||||
* **Maps JavaScript API**: for the interactive maps and street views
|
||||
* **Maps Static API**: for the static map images
|
||||
* **Street View Static API**: for the backend metadata requests
|
||||
|
||||
Required API keys:
|
||||
* **GOOGLE_MAPS_SERVER_API_KEY**: this it used by the backend and should have access to **Street View Static API**
|
||||
* **GOOGLE_MAPS_JS_API_KEY**: this is used by the frontend and should have access to **Maps JavaScript API** and **Maps Static API**
|
||||
|
||||
Additionally, a tile provider is also needed for map editor. This should be configured by `LEAFLET_TILESERVER_URL`, `LEAFLET_TILESERVER_SUBDOMAINS` and `LEAFLET_TILESERVER_ATTRIBUTION`. You can find some providers here: https://wiki.openstreetmap.org/wiki/Tile_servers. OpenStreetMap's tile server is fine for testing.
|
||||
Example:
|
||||
```
|
||||
LEAFLET_TILESERVER_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
LEAFLET_TILESERVER_SUBDOMAINS=abc
|
||||
```
|
||||
|
||||
### (Production only) Create cron job
|
||||
|
||||
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
|
||||
|
||||
```
|
||||
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
|
||||
```
|
||||
|
||||
### Finalize installation
|
||||
|
||||
After you followed the above steps, execute the following command:
|
||||
|
||||
```
|
||||
scripts/install.sh
|
||||
```
|
||||
|
||||
**Warning: Because of a known issue the image `mapguesser_multi` fails to run without the installation steps. You have to relauch `docker-compose up -d` after you finished the installation process.**
|
||||
|
||||
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **mapg@mapg.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
|
||||
|
||||
```
|
||||
./mapg user:add EMAIL PASSWORD admin
|
||||
```
|
||||
|
||||
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
|
||||
|
||||
---
|
||||
|
||||
*License: **GNU AGPL 3.0**. Full license text can be found in file `LICENSE`.*
|
||||
@@ -0,0 +1,26 @@
|
||||
image: php:7.4.7-cli-buster
|
||||
|
||||
pipelines:
|
||||
default:
|
||||
- step:
|
||||
name: Unit Testing
|
||||
caches:
|
||||
- composer
|
||||
artifacts:
|
||||
- unit_test_results.xml
|
||||
script:
|
||||
- apt-get update && apt-get install -y unzip
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
- composer install
|
||||
- vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests
|
||||
- step:
|
||||
name: Static Code Analysis
|
||||
caches:
|
||||
- composer
|
||||
artifacts:
|
||||
- static_code_analysis_results.json
|
||||
script:
|
||||
- apt-get update && apt-get install -y unzip
|
||||
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
- composer install
|
||||
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -4,9 +4,15 @@
|
||||
"description": "MapGuesser Application",
|
||||
"license": "GNU GPL 3.0",
|
||||
"require": {
|
||||
"vlucas/phpdotenv": "^4.1"
|
||||
"vlucas/phpdotenv": "^4.1",
|
||||
"symfony/console": "^5.1",
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"fzaninotto/faker": "^1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9",
|
||||
"phpstan/phpstan": "^0.12.32"
|
||||
},
|
||||
"require-dev": {},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"MapGuesser\\": "src"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?php //empty on purpose
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['id', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'maps');
|
||||
$modify->setId($map['id']);
|
||||
$modify->set('area', $bounds->calculateApproximateArea());
|
||||
$modify->save();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
|
||||
$select = new Select(\Container::$dbConnection, 'users');
|
||||
$select->columns(['id']);
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$modify = new Modify(\Container::$dbConnection, 'users');
|
||||
$modify->setId($map['id']);
|
||||
$modify->set('active', true);
|
||||
$modify->save();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `migrations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`migration` varchar(255) NOT NULL,
|
||||
`type` enum('structure', 'data') NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,17 @@
|
||||
ALTER TABLE
|
||||
`maps`
|
||||
MODIFY
|
||||
`bound_south_lat` decimal(9, 7) NOT NULL,
|
||||
MODIFY
|
||||
`bound_west_lng` decimal(10, 7) NOT NULL,
|
||||
MODIFY
|
||||
`bound_north_lat` decimal(9, 7) NOT NULL,
|
||||
MODIFY
|
||||
`bound_east_lng` decimal(10, 7) NOT NULL;
|
||||
|
||||
ALTER TABLE
|
||||
`places`
|
||||
MODIFY
|
||||
`lat` decimal(9, 7) NOT NULL,
|
||||
MODIFY
|
||||
`lng` decimal(10, 7) NOT NULL;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE
|
||||
`places`
|
||||
ADD
|
||||
`pano_id_cached` varchar(255) NULL DEFAULT NULL,
|
||||
ADD
|
||||
`pano_id_cached_timestamp` timestamp NULL DEFAULT NULL;
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE `users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(100) NOT NULL,
|
||||
`password` varchar(60) NOT NULL,
|
||||
`type` enum('user', 'admin') NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `email` (`email`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE
|
||||
`maps`
|
||||
ADD
|
||||
`area` decimal(13, 4) NOT NULL DEFAULT 0.0;
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `sessions` (
|
||||
`id` varchar(64) NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
`updated` timestamp NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE `user_confirmations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`token` varchar(64) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `token` (`token`),
|
||||
CONSTRAINT `user_confirmations_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
ALTER TABLE
|
||||
`users`
|
||||
ADD
|
||||
`active` tinyint(1) NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE
|
||||
`users`
|
||||
ADD
|
||||
`google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL,
|
||||
ADD
|
||||
UNIQUE `google_sub` (`google_sub`),
|
||||
MODIFY
|
||||
`password` varchar(60) NULL DEFAULT NULL;
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE
|
||||
`places`
|
||||
ADD
|
||||
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
|
||||
ADD
|
||||
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
|
||||
ADD
|
||||
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE `user_password_resetters` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`token` varchar(32) CHARACTER SET ascii NOT NULL,
|
||||
`expires` timestamp NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `token` (`token`),
|
||||
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,5 @@
|
||||
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
|
||||
|
||||
ALTER TABLE `user_confirmations`
|
||||
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `sessions`
|
||||
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
|
||||
|
||||
ALTER TABLE `sessions`
|
||||
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `users`
|
||||
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `multi_rooms` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`room_id` varchar(6) NOT NULL,
|
||||
`state` text NOT NULL,
|
||||
`members` text NOT NULL,
|
||||
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `room_id` (`room_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE `user_played_place` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`place_id` int(10) unsigned NOT NULL,
|
||||
`last_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`occurrences` int(10) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY(`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `place_id` (`place_id`),
|
||||
CONSTRAINT `user_played_place_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `user_played_place_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -1,15 +1,27 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build: ./docker
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-app
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
links:
|
||||
- 'mariadb'
|
||||
multi:
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-multi
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 8090:8090
|
||||
- 9229:9229
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
mariadb:
|
||||
image: mariadb:10.1
|
||||
image: mariadb:10.3
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
environment:
|
||||
@@ -17,5 +29,10 @@ services:
|
||||
MYSQL_DATABASE: 'mapguesser'
|
||||
MYSQL_USER: 'mapguesser'
|
||||
MYSQL_PASSWORD: 'mapguesser'
|
||||
mail:
|
||||
image: marcopas/docker-mailslurper:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 8085:8085
|
||||
volumes:
|
||||
mysql:
|
||||
@@ -4,8 +4,8 @@ ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Install Nginx, PHP and further necessary packages
|
||||
RUN apt update --fix-missing
|
||||
RUN apt install -y curl git mariadb-client nginx \
|
||||
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
|
||||
RUN apt install -y curl git unzip mariadb-client nginx \
|
||||
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php7.4-xml
|
||||
|
||||
# Configure Nginx with PHP
|
||||
RUN mkdir -p /run/php
|
||||
@@ -21,7 +21,7 @@ RUN ./install-composer.sh
|
||||
# Install Node.js and required packages
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt install -y nodejs
|
||||
RUN npm install -g uglify-js clean-css-cli svgo
|
||||
RUN npm install -g uglify-js clean-css-cli svgo yarn
|
||||
|
||||
EXPOSE 80
|
||||
VOLUME /var/www/mapguesser
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM ubuntu:focal
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Install necessary packages
|
||||
RUN apt update --fix-missing
|
||||
RUN apt install -y curl build-essential
|
||||
|
||||
# Install Node.js and required packages
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
|
||||
RUN apt install -y nodejs
|
||||
|
||||
VOLUME /var/www/mapguesser
|
||||
WORKDIR /var/www/mapguesser
|
||||
|
||||
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi
|
||||
@@ -0,0 +1,13 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
|
||||
To reset the password to your account, please click on the following link:<br>
|
||||
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
|
||||
<br><br>
|
||||
You can reset your password with this link util {{EXPIRES}}.
|
||||
<br><br>
|
||||
If you did not requested password reset, no further action is required, your account is not touched.
|
||||
<br><br>
|
||||
Regards,<br>
|
||||
{{APP_NAME}}<br>
|
||||
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
|
||||
<br><br>
|
||||
Have fun on {{APP_NAME}}!
|
||||
<br><br>
|
||||
Regards,<br>
|
||||
{{APP_NAME}}<br>
|
||||
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
|
||||
@@ -0,0 +1,18 @@
|
||||
Hi,
|
||||
<br><br>
|
||||
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
|
||||
To activate your account, please click on the following link:<br>
|
||||
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
|
||||
<br><br>
|
||||
You can activate your account until {{ACTIVATABLE_UNTIL}}.
|
||||
If you don't activate your account, your email address will be permanently deleted after this point of time.
|
||||
<br><br>
|
||||
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
|
||||
However if you want to immediately delete your email address, please click on the following link:<br>
|
||||
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
|
||||
<br><br>
|
||||
Have fun on {{APP_NAME}}!
|
||||
<br><br>
|
||||
Regards,<br>
|
||||
{{APP_NAME}}<br>
|
||||
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>
|
||||
@@ -1,27 +1,23 @@
|
||||
<?php
|
||||
|
||||
define('SCRIPT_STARTED', hrtime(true));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
const ROOT = __DIR__;
|
||||
const VERSION = '';
|
||||
const REVISION = '';
|
||||
const REVISION_DATE = '';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(ROOT);
|
||||
$dotenv->load();
|
||||
|
||||
if (!empty($_ENV['DEV'])) {
|
||||
error_reporting(E_ALL);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
} else {
|
||||
ini_set('display_errors', '0');
|
||||
}
|
||||
|
||||
class Container
|
||||
{
|
||||
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
|
||||
static MapGuesser\Routing\RouteCollection $routeCollection;
|
||||
static MapGuesser\Interfaces\Session\ISessionHandler $sessionHandler;
|
||||
static MapGuesser\Interfaces\Request\IRequest $request;
|
||||
}
|
||||
|
||||
Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
|
||||
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
|
||||
|
||||
session_start();
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require 'main.php';
|
||||
|
||||
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
|
||||
|
||||
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
|
||||
$app->add(new MapGuesser\Cli\AddUserCommand());
|
||||
$app->add(new MapGuesser\Cli\LinkViewCommand());
|
||||
$app->add(new \MapGuesser\Cli\MaintainDatabaseCommand());
|
||||
|
||||
$app->run();
|
||||
@@ -0,0 +1,369 @@
|
||||
'use strict';
|
||||
|
||||
process.title = 'mapguesser-multi';
|
||||
|
||||
class MultiGame {
|
||||
static ROUND_TIMEOUT_DEFAULT = 120000;
|
||||
static ROUND_TIMEOUT_MINIMUM = 15000;
|
||||
static ROUND_TIMEOUT_DIVIDER = 1.5;
|
||||
static ROUND_TIMEOUT_OFFSET = 500;
|
||||
|
||||
constructor() {
|
||||
this.rooms = new Map();
|
||||
}
|
||||
|
||||
cleanupRooms() {
|
||||
this.rooms.forEach(function (room, roomId) {
|
||||
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
if (room.updated < lastValidDate) {
|
||||
this.rooms.delete(roomId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connectToRoom(roomId, token, connection) {
|
||||
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId)
|
||||
var member = room.members.get(token);
|
||||
member.connection = connection;
|
||||
|
||||
this._sendInitialData(room, member, token);
|
||||
}
|
||||
|
||||
createRoom(roomId) {
|
||||
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
joinRoom(roomId, token, userName) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
if (room.members.has(token)) {
|
||||
return { error: 'member_already_joined' };
|
||||
}
|
||||
|
||||
var data = { userName: userName };
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'member_joined', data);
|
||||
});
|
||||
|
||||
room.members.set(token, { userName: userName, connection: null });
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
startGame(roomId, places) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
var rounds = [];
|
||||
places.forEach(function (place) {
|
||||
rounds.push({
|
||||
place: place,
|
||||
results: new Map(),
|
||||
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
|
||||
timeoutStarted: null,
|
||||
timeoutHandler: null
|
||||
})
|
||||
});
|
||||
|
||||
room.rounds = rounds;
|
||||
|
||||
this.nextRound(roomId, 0);
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
guess(roomId, token, guessPosition, distance, score) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
if (round.results.has(token)) {
|
||||
return { error: 'already_guessed' };
|
||||
}
|
||||
|
||||
var member = room.members.get(token);
|
||||
var allResults = this._collectResultsInRound(room, round);
|
||||
|
||||
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
|
||||
|
||||
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
|
||||
|
||||
this._setNewTimeout(room, round);
|
||||
|
||||
return { allResults: allResults };
|
||||
}
|
||||
|
||||
nextRound(roomId, currentRound) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
return { error: 'room_not_found' };
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
room.currentRound = currentRound;
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
|
||||
round.timeoutStarted = new Date();
|
||||
var self = this;
|
||||
round.timeoutHandler = setTimeout(function () {
|
||||
self._endRound(room, round);
|
||||
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
|
||||
|
||||
var data = {};
|
||||
data.place = { panoId: round.place.panoId, pov: round.place.pov };
|
||||
data.timeout = round.timeout;
|
||||
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'new_round', data);
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
_setNewTimeout(room, round) {
|
||||
clearTimeout(round.timeoutHandler);
|
||||
|
||||
if (room.members.size === round.results.size) {
|
||||
round.timeout = 0;
|
||||
round.timeoutStarted = new Date();
|
||||
|
||||
this._endRound(room, round);
|
||||
} else {
|
||||
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
|
||||
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
||||
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
|
||||
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
||||
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
|
||||
}
|
||||
round.timeoutStarted = new Date();
|
||||
var self = this;
|
||||
round.timeoutHandler = setTimeout(function () {
|
||||
self._endRound(room, round);
|
||||
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
|
||||
|
||||
this._broadcastTimeout(room, round);
|
||||
}
|
||||
}
|
||||
|
||||
_endRound(room, round) {
|
||||
var allResults = this._collectResultsInRound(room, round);
|
||||
var self = this;
|
||||
room.members.forEach(function (member, token) {
|
||||
var result = { guessPosition: null, distance: null, score: 0 };
|
||||
|
||||
if (round.results.has(token)) {
|
||||
result = round.results.get(token);
|
||||
} else {
|
||||
round.results.set(token, result);
|
||||
}
|
||||
|
||||
var data = { position: round.place.position, result: result, allResults: allResults };
|
||||
self._sendToMember(member, 'end_round', data);
|
||||
});
|
||||
}
|
||||
|
||||
_sendInitialData(room, member, token) {
|
||||
var data = {};
|
||||
|
||||
if (room.currentRound >= 0) {
|
||||
var round = room.rounds[room.currentRound];
|
||||
|
||||
data.place = round.place;
|
||||
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
|
||||
}
|
||||
|
||||
data.history = [];
|
||||
for (var i = 0; i <= room.currentRound; ++i) {
|
||||
var round = room.rounds[i];
|
||||
|
||||
if (i === room.currentRound && !round.results.has(token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = { guessPosition: null, distance: null, score: 0 };
|
||||
var allResults = [];
|
||||
|
||||
round.results.forEach(function (currentResult, currentToken) {
|
||||
if (token === currentToken) {
|
||||
result = currentResult;
|
||||
return;
|
||||
}
|
||||
|
||||
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
|
||||
});
|
||||
|
||||
data.history.push({
|
||||
position: round.place.position,
|
||||
result: result,
|
||||
allResults: allResults
|
||||
});
|
||||
}
|
||||
|
||||
data.members = [];
|
||||
room.members.forEach(function (currentMember) {
|
||||
data.members.push({ userName: currentMember.userName, me: member === currentMember });
|
||||
});
|
||||
|
||||
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
|
||||
|
||||
this._sendToMember(member, 'initialize', data);
|
||||
}
|
||||
|
||||
_collectResultsInRound(room, round) {
|
||||
var results = [];
|
||||
round.results.forEach(function (result, token) {
|
||||
results.push({
|
||||
userName: room.members.get(token).userName,
|
||||
guessPosition: result.guessPosition,
|
||||
distance: result.distance,
|
||||
score: result.score
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
_broadcastTimeout(room, round) {
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
|
||||
});
|
||||
}
|
||||
|
||||
_broadcastGuess(room, userName, guessPosition, distance, score) {
|
||||
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
|
||||
var round = room.rounds[room.currentRound];
|
||||
var self = this;
|
||||
room.members.forEach(function (member, token) {
|
||||
if (!round.results.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._sendToMember(member, 'guess', data);
|
||||
});
|
||||
}
|
||||
|
||||
_sendToMember(member, type, data) {
|
||||
if (!member.connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.connection.readyState !== ws.OPEN) {
|
||||
member.connection = null;
|
||||
return;
|
||||
}
|
||||
|
||||
member.connection.send(JSON.stringify({ type: type, data: data }));
|
||||
}
|
||||
}
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
var
|
||||
net = require('net'),
|
||||
ws = require('ws');
|
||||
|
||||
var multiGame = new MultiGame();
|
||||
|
||||
//TODO: following should be in a separate class/function
|
||||
|
||||
var tcpServer = net.createServer(function (socket) {
|
||||
socket.on('data', function (data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Cannot parse data: ' + data);
|
||||
return;
|
||||
}
|
||||
|
||||
var response;
|
||||
switch (data.func) {
|
||||
case 'create_room':
|
||||
response = multiGame.createRoom(data.args.roomId);
|
||||
|
||||
break;
|
||||
|
||||
case 'join_room':
|
||||
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
|
||||
|
||||
break;
|
||||
|
||||
case 'start_game':
|
||||
response = multiGame.startGame(data.args.roomId, data.args.places);
|
||||
|
||||
break
|
||||
|
||||
case 'guess':
|
||||
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
|
||||
|
||||
break;
|
||||
|
||||
case 'next_round':
|
||||
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
socket.write(JSON.stringify(response));
|
||||
socket.end();
|
||||
});
|
||||
});
|
||||
tcpServer.on('listening', function () {
|
||||
console.log('[INFO] TCP server started');
|
||||
});
|
||||
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
|
||||
|
||||
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
|
||||
wsServer.on('connection', function (connection, request) {
|
||||
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
|
||||
|
||||
connection.on('message', function (data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Cannot parse data: ' + data);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.func) {
|
||||
case 'connect_to_room':
|
||||
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
connection.on('close', function () {
|
||||
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
|
||||
});
|
||||
});
|
||||
wsServer.on('listening', function () {
|
||||
console.log('[INFO] WS server started');
|
||||
});
|
||||
|
||||
setInterval(function () {
|
||||
multiGame.cleanupRooms();
|
||||
}, 24 * 60 * 60 * 1000);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "mapguesser-multi",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mapguesser-multi",
|
||||
"version": "",
|
||||
"description": "MapGuesser Application - Multiplayer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"ws": "^7.4.4"
|
||||
},
|
||||
"scripts": {},
|
||||
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
|
||||
"license": "GNU AGPL 3.0"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
parameters:
|
||||
level: 5
|
||||
checkMissingIterableValueType: false
|
||||
paths:
|
||||
- main.php
|
||||
- mapg
|
||||
- web.php
|
||||
- src
|
||||
- tests
|
||||
@@ -1,9 +1,7 @@
|
||||
<?php
|
||||
|
||||
require '../main.php';
|
||||
require '../web.php';
|
||||
|
||||
// very basic routing
|
||||
$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'];
|
||||
$method = strtolower($_SERVER['REQUEST_METHOD']);
|
||||
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
|
||||
if (($pos = strpos($url, '?')) !== false) {
|
||||
@@ -11,29 +9,45 @@ if (($pos = strpos($url, '?')) !== false) {
|
||||
}
|
||||
$url = rawurldecode($url);
|
||||
|
||||
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
|
||||
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
|
||||
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
|
||||
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
|
||||
$routeCollection->get('position-json', '{mapId}/position.json', [MapGuesser\Controller\PositionController::class, 'getPosition']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\PositionController::class, 'evaluateGuess']);
|
||||
});
|
||||
|
||||
$match = Container::$routeCollection->match($method, explode('/', $url));
|
||||
$match = Container::$routeCollection->match($method, $url == '' ? [] : explode('/', $url));
|
||||
|
||||
if ($match !== null) {
|
||||
list($route, $params) = $match;
|
||||
|
||||
$response = $route->callController($params);
|
||||
Container::$request->setParsedRouteParams($params);
|
||||
|
||||
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
|
||||
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
|
||||
echo $response->render();
|
||||
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
|
||||
header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode());
|
||||
$handler = $route->getHandler();
|
||||
$controller = new $handler[0](Container::$request);
|
||||
|
||||
if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) {
|
||||
$authorized = $controller->authorize();
|
||||
} else {
|
||||
$authorized = true;
|
||||
}
|
||||
|
||||
if ($method === 'post' && Container::$request->post('anti_csrf_token') !== Container::$request->session()->get('anti_csrf_token')) {
|
||||
$content = new MapGuesser\Response\JsonContent(['error' => 'no_valid_anti_csrf_token']);
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 403);
|
||||
$content->render();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($authorized) {
|
||||
$response = call_user_func([$controller, $handler[1]]);
|
||||
|
||||
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
|
||||
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
|
||||
$response->render();
|
||||
|
||||
return;
|
||||
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
|
||||
header('Location: ' . $response->getUrl(), true, $response->getHttpCode());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 404);
|
||||
require ROOT . '/views/error/404.php';
|
||||
}
|
||||
|
||||
$content = new MapGuesser\Response\HtmlContent('error/404');
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 404);
|
||||
$content->render();
|
||||
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
@@ -0,0 +1,249 @@
|
||||
#panorama {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#panoCover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#guess {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#guess.result {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#guess>#continueButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#continueButtonContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#guess.result>#map {
|
||||
height: calc(100% - 170px);
|
||||
}
|
||||
|
||||
#resultInfo {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
padding: 5px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#resultInfo {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#resultInfo>div {
|
||||
width: 100%;
|
||||
height: 33.33%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#resultInfo p {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoreBarBase {
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#scoreBar {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition-property: width;
|
||||
transition-duration: 2.0s;
|
||||
}
|
||||
|
||||
#showSummaryButton, #startNewGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#startMultiGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#players > p {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#countdown {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 8px;
|
||||
background-color: #eeeeee;
|
||||
border: solid 1px #555555;
|
||||
border-radius: 3px;
|
||||
opacity: 0.95;
|
||||
z-index: 5;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#countdown.yellow {
|
||||
background-color: #f7c789;
|
||||
border: solid 1px #e8a349;
|
||||
}
|
||||
|
||||
#countdown.red {
|
||||
background-color: #f7a5a5;
|
||||
border: solid 1px #aa5e5e;
|
||||
}
|
||||
|
||||
#countdown p {
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#countdown.yellow p {
|
||||
color: #9c4308;
|
||||
}
|
||||
|
||||
#countdown.red p {
|
||||
color: #701919;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
#mapName {
|
||||
display: none;
|
||||
}
|
||||
#showGuessButtonContainer {
|
||||
position: absolute;
|
||||
left: 65px;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
#guess {
|
||||
top: 10px;
|
||||
left: 20px;
|
||||
opacity: 0.95;
|
||||
visibility: hidden;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 90px);
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 100%;
|
||||
}
|
||||
#navigation {
|
||||
bottom: 25px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
#showGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
#guess {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
#guess.adapt {
|
||||
top: initial;
|
||||
width: 250px;
|
||||
height: 200px;
|
||||
opacity: 0.5;
|
||||
transition-property: width, height, opacity;
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0.8s;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
#closeGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 45px);
|
||||
}
|
||||
#guess.result {
|
||||
width: initial;
|
||||
height: initial;
|
||||
top: 10px;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
bottom: 50px;
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 60%;
|
||||
}
|
||||
#navigation {
|
||||
bottom: 50px;
|
||||
left: 20px;
|
||||
}
|
||||
@media screen and (max-height: 424px) {
|
||||
#guess {
|
||||
top: 10px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
top: 10px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.result {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 30px;
|
||||
}
|
||||
#navigation {
|
||||
bottom: 30px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
.map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#mapSelection img {
|
||||
display: inline;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
/* modify the cursor for the Leaflet map */
|
||||
.leaflet-container {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
#panorama {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#noPano {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
visibility: hidden;
|
||||
background: #cccccc;
|
||||
}
|
||||
|
||||
#noPano>p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#control {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 125px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#placeControl {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
z-index: 3;
|
||||
width: 100px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#overlayControl {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#deleteButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 999px) and (min-height: 600px) {
|
||||
#map.selected {
|
||||
height: 50%;
|
||||
}
|
||||
#panorama, #noPano {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
}
|
||||
#placeControl {
|
||||
top: calc(50% + 10px);
|
||||
}
|
||||
.hideOnMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px), (max-height: 599px) {
|
||||
#map.selected {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
}
|
||||
#panorama, #noPano {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
}
|
||||
#placeControl {
|
||||
top: 10px;
|
||||
}
|
||||
#modified.selected {
|
||||
right: calc(50% + 10px);
|
||||
}
|
||||
#control.selected {
|
||||
right: calc(50% + 10px);
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,26 @@ html, body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner, input::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
p, h1, h2, button, a {
|
||||
/* to be compatible with browsers that don't know <main> */
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #28a745;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
p, h1, h2, input, textarea, select, button, a {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
@@ -37,7 +51,7 @@ h1>a:hover, h1>a:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h2, div.header.small h1 {
|
||||
h2, header.small h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@@ -69,16 +83,17 @@ sub {
|
||||
bottom: -0.4em;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
hr {
|
||||
border: solid #bbbbbb 1px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 12px;
|
||||
p.small, span.small {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.justify {
|
||||
@@ -89,11 +104,31 @@ sub {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.marginLeft {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.marginBottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
svg.inline {
|
||||
.marginRight {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
svg.inline, img.inline {
|
||||
display: inline;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.3em;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
@@ -122,6 +157,27 @@ button, a.button {
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
button.small, div.inputWithButton>button {
|
||||
font-size: 14px;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
button.small {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
div.inputWithButton>button {
|
||||
border-radius: 2px;
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
width: 75px;
|
||||
margin-left: -79px;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
|
||||
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
|
||||
background-color: #29457f;
|
||||
outline: none;
|
||||
@@ -139,15 +195,158 @@ button.fullWidth, a.button.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button.noLeftRadius, a.button.noLeftRadius {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
button.noRightRadius, a.button.noRightRadius {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button.gray, a.button.gray {
|
||||
background-color: #808080;
|
||||
}
|
||||
|
||||
button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
|
||||
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
div.header {
|
||||
button.red, a.button.red {
|
||||
background-color: #aa5e5e;
|
||||
}
|
||||
|
||||
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
|
||||
background-color: #7f2929;
|
||||
}
|
||||
|
||||
button.yellow, a.button.yellow {
|
||||
background-color: #e8a349;
|
||||
}
|
||||
|
||||
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
|
||||
background-color: #c37713;
|
||||
}
|
||||
|
||||
button.green, a.button.green {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus {
|
||||
background-color: #1b7d31;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
background-color: #f9fafb;
|
||||
border: solid #c8d2e1 1px;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
input, select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: 5px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input.big, select.big, textarea.big, div.inputWithButton>input {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input.big, select.big, div.inputWithButton>input {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
textarea.big {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
input.fullWidth, select.fullWidth, textarea.fullWidth {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:disabled, select:disabled, textarea:disabled {
|
||||
background-color: #dfdfdf;
|
||||
border: solid #dfdfdf 1px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
background-color: #ffffff;
|
||||
border: solid #29457f 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
input.big:focus, select.big:focus {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input {
|
||||
width: 100%;
|
||||
padding: 0 83px 0 6px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input:focus {
|
||||
padding: 0 82px 0 5px;
|
||||
}
|
||||
|
||||
textarea.big:focus {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.modal {
|
||||
position: fixed;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
z-index: 6;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 5;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p.error, p.formError {
|
||||
color: #7f2929;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p.formError {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
background-color: #333333;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
@@ -155,20 +354,52 @@ div.header {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.header>div.grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
div.header.small {
|
||||
header.small {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
div.main {
|
||||
header>p {
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
header>p>span {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
header>p>span>a:link, header>p>span>a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
header>p>span:not(:last-child) {
|
||||
border-right: solid white 1px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: #ffffff;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
main.full {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #444444;
|
||||
padding: 6px 12px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer>p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
div.buttonContainer {
|
||||
height: 35px;
|
||||
}
|
||||
@@ -177,287 +408,161 @@ div.buttonContainer>button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
div.mapContainer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
div.mapItem {
|
||||
width: 350px;
|
||||
#cookiesNotice {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 20px;
|
||||
background-color: #eeeeee;
|
||||
border: solid #888888 1px;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p.title {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
div.mapItem>img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
text-align: right;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -32px;
|
||||
margin-left: -32px;
|
||||
z-index: 5;
|
||||
z-index: 7;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#roundInfo {
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#roundInfo p {
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#panorama {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#cover {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#guess {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#guess.result {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#guess>#continueButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#continueButtonContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#guess.result>#map {
|
||||
height: calc(100% - 170px);
|
||||
}
|
||||
|
||||
#resultInfo {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
padding: 5px 20px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
background-color: #ffffff;
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#guess.result>#resultInfo {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#resultInfo>div {
|
||||
width: 100%;
|
||||
height: 33.33%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#resultInfo p {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoreBarBase {
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
div.box {
|
||||
width: 576px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#scoreBar {
|
||||
width: 0;
|
||||
.circleControl {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.circleControl .controlItem {
|
||||
height: 60px;
|
||||
margin-top: 10px;
|
||||
opacity: 70%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.circleControl .controlItem:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.circleControl .controlItem div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.circleControl .controlBackground {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition-property: width;
|
||||
transition-duration: 2.0s;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
#showSummaryButton, #startNewGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1134px) and (max-width: 1503px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 764px) and (max-width: 1133px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 763px) {
|
||||
div.mapContainer {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 374px) {
|
||||
div.mapItem {
|
||||
width: initial;
|
||||
}
|
||||
.circleControl .controlIcon {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
margin: auto;
|
||||
margin-top: 50%;
|
||||
transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
div.header.small h1 span {
|
||||
header h1 span {
|
||||
display: none;
|
||||
}
|
||||
button {
|
||||
footer>p:not(:first-child) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
button, a.button {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#showGuessButtonContainer {
|
||||
position: absolute;
|
||||
button.marginLeft, a.button.marginLeft {
|
||||
margin-left: 0;
|
||||
}
|
||||
button.marginRight, a.button.marginRight {
|
||||
margin-right: 0;
|
||||
}
|
||||
div.modal {
|
||||
left: 20px;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
#guess {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
opacity: 0.95;
|
||||
visibility: hidden;
|
||||
div.box {
|
||||
width: initial;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 90px);
|
||||
.circleControl {
|
||||
width: 45px;
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 100%;
|
||||
.circleControl .controlItem {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
#showGuessButtonContainer {
|
||||
display: none;
|
||||
footer>p {
|
||||
display: inline;
|
||||
}
|
||||
#guess {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
footer>p:not(:first-child) {
|
||||
padding-left: 6px;
|
||||
}
|
||||
#guess.adapt {
|
||||
top: initial;
|
||||
width: 250px;
|
||||
height: 200px;
|
||||
opacity: 0.5;
|
||||
transition-property: width, height, opacity;
|
||||
transition-duration: 0.1s;
|
||||
transition-delay: 0.8s;
|
||||
footer>p:not(:last-child) {
|
||||
border-right: solid white 1px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
width: 500px;
|
||||
height: 375px;
|
||||
opacity: 0.95;
|
||||
transition-delay: 0s;
|
||||
div.modal {
|
||||
width: 540px;
|
||||
left: 50%;
|
||||
margin-left: -270px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
#closeGuessButtonContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-height: 399px) {
|
||||
div.modal {
|
||||
top: 20px;
|
||||
max-height: calc(100% - 40px);
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#map {
|
||||
height: calc(100% - 45px);
|
||||
.circleControl {
|
||||
width: 45px;
|
||||
}
|
||||
#guess.result {
|
||||
width: initial;
|
||||
height: initial;
|
||||
.circleControl .controlItem {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 400px) and (max-height: 499px) {
|
||||
div.modal {
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
bottom: 50px;
|
||||
}
|
||||
#scoreBarBase {
|
||||
width: 60%;
|
||||
max-height: calc(100% - 100px);
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
@media screen and (max-height: 424px) {
|
||||
#guess {
|
||||
top: 50px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.adapt:hover {
|
||||
top: 50px;
|
||||
height: initial;
|
||||
}
|
||||
#guess.result {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 500px) {
|
||||
div.modal {
|
||||
top: 75px;
|
||||
max-height: calc(100% - 150px);
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#mapContainer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
div.mapItem {
|
||||
width: 350px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
div.mapItem.new {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
padding: 0 8px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p {
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div.mapItem>div.title>p.title {
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
div.mapItem>div.imgContainer {
|
||||
width: 100%;
|
||||
padding-top: 50%;
|
||||
background-color: #cccccc;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner {
|
||||
background-color: #eeeeee;
|
||||
padding: 10px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.description {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(1) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.mapItem>div.buttonContainer {
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1134px) and (max-width: 1503px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 764px) and (max-width: 1133px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 763px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 374px) {
|
||||
div.mapItem {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
|
||||
<circle cx="50%" cy="50%" r="50%" fill="black" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 146 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<polygon id="needleNorth" points="50,0 33,50 66,50" style="fill:red;stroke:black;stroke-width:1" />
|
||||
<polygon id="needleSouth" points="50,100 33,50 66,50" style="fill:white;stroke:black;stroke-width:1" />
|
||||
Sorry, your browser does not support inline SVG.
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1,2 @@
|
||||
The PNGs in this folder are generated from '../icon.svg'.
|
||||
Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'.
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="#3183ce" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
|
||||
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 556 B |
@@ -1,4 +1,3 @@
|
||||
<!-- The PNGs in this folder are generated from this SVG. -->
|
||||
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" />
|
||||
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 530 B |
@@ -0,0 +1,10 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#3183ce"
|
||||
fill-rule="evenodd"
|
||||
stroke="#19456d"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#3183ce"
|
||||
fill-rule="evenodd"
|
||||
stroke="#19456d"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#19456d"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 639 B |
@@ -0,0 +1,10 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#828282"
|
||||
fill-rule="evenodd"
|
||||
stroke="#383838"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#828282"
|
||||
fill-rule="evenodd"
|
||||
stroke="#383838"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#383838"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 639 B |
@@ -0,0 +1,10 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#28a745"
|
||||
fill-rule="evenodd"
|
||||
stroke="#285624"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#28a745"
|
||||
fill-rule="evenodd"
|
||||
stroke="#285624"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#285624"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 639 B |
@@ -0,0 +1,16 @@
|
||||
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="#d24d4d"
|
||||
fill-rule="evenodd"
|
||||
stroke="#752929"
|
||||
stroke-width="0.3"
|
||||
stroke-linecap="round"
|
||||
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
|
||||
<circle
|
||||
fill="#752929"
|
||||
fill-rule="evenodd"
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 639 B |
@@ -0,0 +1,5 @@
|
||||
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
|
||||
<path d="m14.12 10.163 1.715.858c.22.11.22.424 0 .534L8.267 15.34a.598.598 0 0 1-.534 0L.165 11.555a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.66zM7.733.063a.598.598 0 0 1 .534 0l7.568 3.784a.3.3 0 0 1 0 .535L8.267 8.165a.598.598 0 0 1-.534 0L.165 4.382a.299.299 0 0 1 0-.535L7.733.063z"/>
|
||||
<path d="m14.12 6.576 1.715.858c.22.11.22.424 0 .534l-7.568 3.784a.598.598 0 0 1-.534 0L.165 7.968a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.659z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 718 B |
@@ -0,0 +1,69 @@
|
||||
var Account = {
|
||||
original: null,
|
||||
countdown: null,
|
||||
|
||||
openGoogleAuthenticate: function () {
|
||||
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
|
||||
},
|
||||
|
||||
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
|
||||
var password = document.getElementsByTagName('form')[0].elements.password;
|
||||
var button = document.getElementById('authenticateWithGoogleButton');
|
||||
|
||||
Account.original = {
|
||||
type: password.type,
|
||||
placeholder: password.placeholder,
|
||||
required: password.required,
|
||||
disabled: password.disabled
|
||||
};
|
||||
|
||||
password.type = 'text';
|
||||
password.placeholder = 'Authenticated!'
|
||||
password.value = '';
|
||||
password.required = false;
|
||||
password.disabled = true;
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
Account.countdown = setInterval(function () {
|
||||
var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000);
|
||||
|
||||
if (timeLeft > 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
Account.resetGoogleAuthentication();
|
||||
return;
|
||||
}
|
||||
|
||||
password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...';
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
resetGoogleAuthentication: function () {
|
||||
if (Account.countdown !== null) {
|
||||
clearInterval(Account.countdown);
|
||||
}
|
||||
|
||||
var password = document.getElementsByTagName('form')[0].elements.password;
|
||||
var button = document.getElementById('authenticateWithGoogleButton');
|
||||
|
||||
password.type = Account.original.type;
|
||||
password.placeholder = Account.original.placeholder
|
||||
password.required = Account.original.required;
|
||||
password.disabled = Account.original.disabled;
|
||||
|
||||
button.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
(function () {
|
||||
document.getElementById('authenticateWithGoogleButton').onclick = function () {
|
||||
Account.openGoogleAuthenticate();
|
||||
};
|
||||
|
||||
document.getElementsByTagName('form')[0].onreset = function () {
|
||||
Account.resetGoogleAuthentication();
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,10 @@
|
||||
(function () {
|
||||
if (success) {
|
||||
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
|
||||
window.close();
|
||||
} else {
|
||||
document.getElementById('closeWindowButton').onclick = function () {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,9 @@
|
||||
(function () {
|
||||
document.getElementById('cancelGoogleSignupButton').onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
|
||||
window.location.replace('/signup');
|
||||
});
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,12 @@
|
||||
(function () {
|
||||
var resetSignupButton = document.getElementById('resetSignupButton');
|
||||
if (resetSignupButton) {
|
||||
resetSignupButton.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/signup/reset', function () {
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,739 @@
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
var MapEditor = {
|
||||
metadata: {
|
||||
name: null,
|
||||
description: null
|
||||
},
|
||||
map: null,
|
||||
panorama: null,
|
||||
selectedMarker: null,
|
||||
added: {},
|
||||
edited: {},
|
||||
deleted: {},
|
||||
|
||||
editMetadata: function () {
|
||||
var form = document.getElementById('metadataForm');
|
||||
|
||||
MapEditor.metadata.name = form.elements.name.value;
|
||||
MapEditor.metadata.description = form.elements.description.value;
|
||||
|
||||
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
|
||||
|
||||
MapGuesser.hideModal();
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
getPlace: function (placeId, marker) {
|
||||
MapGuesser.httpRequest('GET', '/admin/place.json/' + placeId, function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (!this.response.panoId) {
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
|
||||
places[marker.placeId].panoId = -1;
|
||||
places[marker.placeId].noPano = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
places[marker.placeId].panoId = this.response.panoId;
|
||||
places[marker.placeId].noPano = false;
|
||||
|
||||
MapEditor.loadPano(this.response.panoId, places[marker.placeId].pov);
|
||||
});
|
||||
},
|
||||
|
||||
loadPano: function (panoId, pov) {
|
||||
MapEditor.panorama.setVisible(true);
|
||||
MapEditor.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
|
||||
MapEditor.panorama.setZoom(pov.zoom);
|
||||
MapEditor.panorama.setPano(panoId);
|
||||
},
|
||||
|
||||
loadPanoForNewPlace: function (panoLocationData) {
|
||||
var placeId = MapEditor.selectedMarker.placeId;
|
||||
|
||||
if (!panoLocationData) {
|
||||
places[placeId].panoId = -1;
|
||||
places[placeId].noPano = true;
|
||||
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var latLng = panoLocationData.latLng;
|
||||
|
||||
places[placeId].panoId = panoLocationData.pano;
|
||||
places[placeId].lat = latLng.lat();
|
||||
places[placeId].lng = latLng.lng();
|
||||
|
||||
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
|
||||
MapEditor.map.panTo(MapEditor.selectedMarker.getLatLng());
|
||||
|
||||
MapEditor.panorama.setVisible(true);
|
||||
MapEditor.panorama.setPov({ heading: 0.0, pitch: 0.0 });
|
||||
MapEditor.panorama.setZoom(0.0);
|
||||
MapEditor.panorama.setPano(panoLocationData.pano);
|
||||
},
|
||||
|
||||
requestPanoData: function (location, canBeIndoor) {
|
||||
var sv = new google.maps.StreetViewService();
|
||||
|
||||
sv.getPanorama({
|
||||
location: location,
|
||||
preference: google.maps.StreetViewPreference.NEAREST,
|
||||
radius: MapEditor.map.getSearchRadius(location),
|
||||
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
|
||||
}, function (data, status) {
|
||||
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
|
||||
|
||||
if (panoLocationData === null && !canBeIndoor) {
|
||||
MapEditor.requestPanoData(location, true);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
MapEditor.loadPanoForNewPlace(panoLocationData);
|
||||
});
|
||||
},
|
||||
|
||||
select: function (marker) {
|
||||
if (MapEditor.selectedMarker === marker) {
|
||||
MapEditor.closePlace();
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('map').classList.add('selected');
|
||||
document.getElementById('control').classList.add('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.visibility = 'visible';
|
||||
document.getElementById('placeControl').style.visibility = 'visible';
|
||||
|
||||
MapEditor.resetSelected();
|
||||
MapEditor.selectedMarker = marker;
|
||||
|
||||
MapEditor.map.resize();
|
||||
MapEditor.map.panTo(marker.getLatLng());
|
||||
|
||||
MapEditor.panorama.setVisible(false);
|
||||
|
||||
if (marker.placeId) {
|
||||
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'block';
|
||||
|
||||
if (places[marker.placeId].panoId) {
|
||||
if (places[marker.placeId].panoId === -1) {
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
} else {
|
||||
MapEditor.loadPano(places[marker.placeId].panoId, places[marker.placeId].pov);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapEditor.getPlace(marker.placeId, marker);
|
||||
} else {
|
||||
marker.placeId = 'new_' + new Date().getTime();
|
||||
|
||||
var latLng = marker.getLatLng();
|
||||
|
||||
places[marker.placeId] = { id: null, lat: latLng.lat, lng: latLng.lng, panoId: null, pov: { heading: 0.0, pitch: 0.0, zoom: 0 }, noPano: false };
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapEditor.requestPanoData(latLng);
|
||||
}
|
||||
},
|
||||
|
||||
resetSelected: function (del) {
|
||||
if (!MapEditor.selectedMarker) {
|
||||
return;
|
||||
}
|
||||
|
||||
var placeId = MapEditor.selectedMarker.placeId
|
||||
|
||||
if (places[placeId].id && !del) {
|
||||
MapEditor.map.changeMarkerIcon(
|
||||
MapEditor.selectedMarker,
|
||||
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
|
||||
);
|
||||
} else {
|
||||
delete places[placeId];
|
||||
MapEditor.map.removeMarker(MapEditor.selectedMarker);
|
||||
}
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'none';
|
||||
},
|
||||
|
||||
applyPlace: function () {
|
||||
var placeId = MapEditor.selectedMarker.placeId;
|
||||
|
||||
if (!places[placeId].noPano) {
|
||||
var latLng = MapEditor.panorama.getPosition();
|
||||
var pov = MapEditor.panorama.getPov();
|
||||
var zoom = MapEditor.panorama.getZoom();
|
||||
|
||||
places[placeId].lat = latLng.lat();
|
||||
places[placeId].lng = latLng.lng();
|
||||
places[placeId].panoId = MapEditor.panorama.getPano();
|
||||
places[placeId].pov = { heading: pov.heading, pitch: pov.pitch, zoom: zoom };
|
||||
}
|
||||
|
||||
if (!places[placeId].id) {
|
||||
places[placeId].id = placeId;
|
||||
MapEditor.added[placeId] = places[placeId];
|
||||
|
||||
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'block';
|
||||
} else {
|
||||
if (!MapEditor.added[placeId]) {
|
||||
MapEditor.edited[placeId] = places[placeId];
|
||||
|
||||
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
|
||||
} else {
|
||||
MapEditor.added[placeId] = places[placeId];
|
||||
}
|
||||
}
|
||||
|
||||
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
closePlace: function (del) {
|
||||
document.getElementById('map').classList.remove('selected');
|
||||
document.getElementById('control').classList.remove('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.visibility = 'hidden';
|
||||
document.getElementById('placeControl').style.visibility = 'hidden';
|
||||
|
||||
MapEditor.resetSelected(del);
|
||||
MapEditor.selectedMarker = null;
|
||||
|
||||
MapEditor.map.resize();
|
||||
},
|
||||
|
||||
deletePlace: function () {
|
||||
var placeId = MapEditor.selectedMarker.placeId;
|
||||
|
||||
if (places[placeId].id && !MapEditor.added[placeId]) {
|
||||
MapEditor.deleted[placeId] = places[placeId];
|
||||
|
||||
document.getElementById('deleted').innerHTML = String(Object.keys(MapEditor.deleted).length);
|
||||
}
|
||||
|
||||
MapEditor.closePlace(true);
|
||||
|
||||
delete MapEditor.added[placeId];
|
||||
delete MapEditor.edited[placeId];
|
||||
delete places[placeId];
|
||||
|
||||
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
|
||||
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
saveMap: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
var data = new FormData();
|
||||
|
||||
if (MapEditor.metadata.name !== null) {
|
||||
data.append('name', MapEditor.metadata.name);
|
||||
}
|
||||
if (MapEditor.metadata.description !== null) {
|
||||
data.append('description', MapEditor.metadata.description);
|
||||
}
|
||||
|
||||
for (var placeId in MapEditor.added) {
|
||||
if (!MapEditor.added.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
data.append('added[]', JSON.stringify(MapEditor.added[placeId]));
|
||||
}
|
||||
for (var placeId in MapEditor.edited) {
|
||||
if (!MapEditor.edited.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
data.append('edited[]', JSON.stringify(MapEditor.edited[placeId]));
|
||||
}
|
||||
for (var placeId in MapEditor.deleted) {
|
||||
if (!MapEditor.deleted.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
data.append('deleted[]', JSON.stringify(MapEditor.deleted[placeId]));
|
||||
}
|
||||
|
||||
MapGuesser.httpRequest('POST', '/admin/saveMap/' + mapId + '/json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
//TODO: handle this error
|
||||
return;
|
||||
}
|
||||
|
||||
MapEditor.replacePlaceIdsToReal(this.response.added);
|
||||
|
||||
if (mapId === 0) {
|
||||
mapId = this.response.mapId;
|
||||
window.history.replaceState(null, '', '/admin/mapEditor/' + mapId);
|
||||
}
|
||||
|
||||
MapEditor.added = {};
|
||||
MapEditor.edited = {};
|
||||
MapEditor.deleted = {};
|
||||
|
||||
document.getElementById('added').innerHTML = '0';
|
||||
document.getElementById('edited').innerHTML = '0';
|
||||
document.getElementById('deleted').innerHTML = '0';
|
||||
|
||||
document.getElementById('saveButton').disabled = true;
|
||||
}, data);
|
||||
},
|
||||
|
||||
replacePlaceIdsToReal: function (addedPlaces) {
|
||||
for (var i = 0; i < addedPlaces.length; ++i) {
|
||||
var tempId = addedPlaces[i].tempId;
|
||||
var placeId = addedPlaces[i].id;
|
||||
places[tempId].id = placeId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var Util = {
|
||||
getHighResData: function () {
|
||||
if (window.devicePixelRatio >= 4) {
|
||||
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
|
||||
} else if (window.devicePixelRatio >= 2) {
|
||||
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
|
||||
} else {
|
||||
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
|
||||
}
|
||||
},
|
||||
|
||||
extractCoordinates: function (coordinatesStr) {
|
||||
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
|
||||
var delimiters = [',', ' ', ';'];
|
||||
|
||||
coordinatesStr = coordinatesStr.trim();
|
||||
if (coordinatesStr.length == 0) {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
for (var delimiter of delimiters) {
|
||||
if (coordinatesStr.indexOf(delimiter) != -1) {
|
||||
|
||||
var coordinatesArr = coordinatesStr.split(delimiter);
|
||||
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
|
||||
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
|
||||
|
||||
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
|
||||
coordinates.valid = true;
|
||||
return coordinates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
};
|
||||
|
||||
var LMapWrapper = {
|
||||
map: null,
|
||||
markers: null,
|
||||
divId: null,
|
||||
iconCollection: {
|
||||
iconGreen: L.icon({
|
||||
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
iconRed: L.icon({
|
||||
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
iconBlue: L.icon({
|
||||
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
})
|
||||
},
|
||||
|
||||
init: function (divId, places) {
|
||||
document.getElementById(divId).style.display = "block";
|
||||
|
||||
if (!LMapWrapper.map) {
|
||||
LMapWrapper.divId = divId;
|
||||
LMapWrapper.map = L.map(LMapWrapper.divId, {
|
||||
center: { lat: 0., lng: 0. },
|
||||
zoom: 2
|
||||
});
|
||||
|
||||
LMapWrapper.map.on('click', function (e) {
|
||||
LMapWrapper.placeMarker(e.latlng);
|
||||
});
|
||||
|
||||
var highResData = Util.getHighResData();
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
attribution: tileAttribution,
|
||||
subdomains: tileSubdomains,
|
||||
ppi: highResData.ppi,
|
||||
tileSize: highResData.tileSize,
|
||||
zoomOffset: highResData.zoomOffset,
|
||||
minZoom: highResData.minZoom,
|
||||
maxZoom: highResData.maxZoom
|
||||
}).addTo(LMapWrapper.map);
|
||||
|
||||
if (mapId) {
|
||||
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||
}
|
||||
}
|
||||
|
||||
LMapWrapper.loadMarkers(places);
|
||||
|
||||
document.getElementById('streetViewCoverSelector').style.display = 'none';
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
document.getElementById(LMapWrapper.divId).style.display = 'none';
|
||||
},
|
||||
|
||||
loadMarkers: function (places) {
|
||||
if (!LMapWrapper.markers) {
|
||||
LMapWrapper.markers = L.markerClusterGroup({
|
||||
maxClusterRadius: 50
|
||||
});
|
||||
} else {
|
||||
LMapWrapper.markers.clearLayers();
|
||||
}
|
||||
|
||||
for (var placeId in places) {
|
||||
if (!places.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var place = places[placeId];
|
||||
|
||||
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
|
||||
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
|
||||
zIndexOffset: 1000
|
||||
})
|
||||
.addTo(LMapWrapper.markers)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
marker.placeId = placeId;
|
||||
}
|
||||
|
||||
LMapWrapper.map.addLayer(LMapWrapper.markers);
|
||||
},
|
||||
|
||||
// TODO: check whether marker is already existing on the map for the coordinates
|
||||
// or alternatively block saving for matching coordinates
|
||||
placeMarker: function (latLng) {
|
||||
var marker = L.marker(latLng, {
|
||||
icon: LMapWrapper.iconCollection.iconBlue,
|
||||
zIndexOffset: 2000
|
||||
})
|
||||
.addTo(LMapWrapper.markers)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
MapEditor.select(marker);
|
||||
},
|
||||
|
||||
panTo: function (latLng) {
|
||||
LMapWrapper.map.panTo(latLng);
|
||||
},
|
||||
|
||||
resize: function () {
|
||||
LMapWrapper.map.invalidateSize(true);
|
||||
},
|
||||
|
||||
changeMarkerIcon: function (marker, icon) {
|
||||
marker.setIcon(icon);
|
||||
marker.setZIndexOffset(2000);
|
||||
},
|
||||
|
||||
removeMarker: function (marker) {
|
||||
LMapWrapper.markers.removeLayer(marker);
|
||||
},
|
||||
|
||||
toggleStreetViewCover: function () { },
|
||||
|
||||
getSearchRadius: function (location) {
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
|
||||
var GMapWrapper = {
|
||||
map: null,
|
||||
markers: null,
|
||||
divId: null,
|
||||
streetViewCover: null,
|
||||
streetViewCoverOn: false,
|
||||
iconCollection: {
|
||||
iconGreen: {
|
||||
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
|
||||
scaledSize: new google.maps.Size(24, 32), // scaled size
|
||||
origin: new google.maps.Point(0, 0), // origin
|
||||
anchor: new google.maps.Point(12, 32) // anchor
|
||||
},
|
||||
iconRed: {
|
||||
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
|
||||
scaledSize: new google.maps.Size(24, 32), // scaled size
|
||||
origin: new google.maps.Point(0, 0), // origin
|
||||
anchor: new google.maps.Point(12, 32) // anchor
|
||||
},
|
||||
iconBlue: {
|
||||
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
|
||||
scaledSize: new google.maps.Size(24, 32), // scaled size
|
||||
origin: new google.maps.Point(0, 0), // origin
|
||||
anchor: new google.maps.Point(12, 32) // anchor
|
||||
}
|
||||
},
|
||||
|
||||
init: function (divId, places) {
|
||||
document.getElementById(divId).style.display = "block";
|
||||
|
||||
if (!GMapWrapper.map) {
|
||||
GMapWrapper.divId = divId;
|
||||
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
|
||||
center: { lat: 0., lng: 0. },
|
||||
zoom: 2,
|
||||
fullscreenControl: false,
|
||||
zoomControl: true,
|
||||
zoomControlOptions: {
|
||||
position: google.maps.ControlPosition.LEFT_BOTTOM
|
||||
},
|
||||
streetViewControl: false,
|
||||
draggableCursor: 'crosshair'
|
||||
});
|
||||
|
||||
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
|
||||
|
||||
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
|
||||
GMapWrapper.placeMarker({
|
||||
lat: mapsMouseEvent.latLng.lat(),
|
||||
lng: mapsMouseEvent.latLng.lng()
|
||||
});
|
||||
});
|
||||
|
||||
if (mapId) {
|
||||
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
|
||||
}
|
||||
}
|
||||
|
||||
GMapWrapper.loadMarkers(places);
|
||||
|
||||
document.getElementById('streetViewCoverSelector').style.display = 'block'
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
document.getElementById(GMapWrapper.divId).style.display = 'none';
|
||||
},
|
||||
|
||||
loadMarkers: function (places) {
|
||||
if (!GMapWrapper.markers) {
|
||||
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
|
||||
imagePath: STATIC_ROOT + '/img/markers/m',
|
||||
imageExtension: 'png?rev' + REVISION
|
||||
});
|
||||
} else {
|
||||
GMapWrapper.markers.clearMarkers();
|
||||
}
|
||||
|
||||
for (var placeId in places) {
|
||||
if (!places.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var place = places[placeId];
|
||||
|
||||
var marker = new google.maps.Marker({
|
||||
position: {
|
||||
lat: place.lat,
|
||||
lng: place.lng
|
||||
},
|
||||
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
|
||||
});
|
||||
|
||||
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
|
||||
marker.setLatLng = function (coords) { this.setPosition(coords) };
|
||||
|
||||
marker.addListener('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
marker.placeId = placeId;
|
||||
|
||||
GMapWrapper.markers.addMarker(marker);
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: check whether marker is already existing on the map for the coordinates
|
||||
// or alternatively block saving for matching coordinates
|
||||
placeMarker: function (latLng) {
|
||||
var marker = new google.maps.Marker({
|
||||
map: GMapWrapper.map,
|
||||
position: latLng,
|
||||
icon: GMapWrapper.iconCollection.iconBlue,
|
||||
});
|
||||
|
||||
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
|
||||
marker.setLatLng = function (coords) { this.setPosition(coords) };
|
||||
|
||||
marker.addListener('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
GMapWrapper.markers.addMarker(marker);
|
||||
|
||||
MapEditor.select(marker);
|
||||
},
|
||||
|
||||
panTo: function (latLng) {
|
||||
GMapWrapper.map.panTo(latLng);
|
||||
},
|
||||
|
||||
resize: function () {
|
||||
google.maps.event.trigger(GMapWrapper.map, 'resize');
|
||||
},
|
||||
|
||||
changeMarkerIcon: function (marker, icon) {
|
||||
marker.setIcon(icon);
|
||||
},
|
||||
|
||||
removeMarker: function (marker) {
|
||||
GMapWrapper.markers.removeMarker(marker);
|
||||
},
|
||||
|
||||
toggleStreetViewCover: function () {
|
||||
if (GMapWrapper.streetViewCoverOn) {
|
||||
GMapWrapper.streetViewCover.setMap(null);
|
||||
GMapWrapper.streetViewCoverOn = false;
|
||||
} else {
|
||||
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
|
||||
GMapWrapper.streetViewCoverOn = true;
|
||||
}
|
||||
},
|
||||
|
||||
getSearchRadius: function (location) {
|
||||
// source: https://www.yorku.ca/mack/CHI01.htm
|
||||
var movementOffset = 4;
|
||||
|
||||
// source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1
|
||||
var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom());
|
||||
|
||||
var minSearchRadius = 5;
|
||||
|
||||
var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel));
|
||||
|
||||
return searchRadius;
|
||||
}
|
||||
};
|
||||
|
||||
// initialize content of #map with google maps
|
||||
MapEditor.map = GMapWrapper;
|
||||
MapEditor.map.init('gmap', places);
|
||||
|
||||
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
||||
// switch off fullscreenControl because positioning doesn't work
|
||||
fullscreenControl: false,
|
||||
fullscreenControlOptions: {
|
||||
position: google.maps.ControlPosition.LEFT_TOP
|
||||
},
|
||||
motionTracking: false
|
||||
});
|
||||
|
||||
document.getElementById('mapName').onclick = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
MapGuesser.showModal('metadata');
|
||||
document.getElementById('metadataForm').elements.name.select();
|
||||
};
|
||||
|
||||
document.getElementById('metadataForm').onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
MapEditor.editMetadata();
|
||||
};
|
||||
|
||||
document.getElementById('closeMetadataButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
document.getElementById('saveButton').onclick = function () {
|
||||
MapEditor.saveMap();
|
||||
};
|
||||
|
||||
document.getElementById('applyButton').onclick = function () {
|
||||
MapEditor.applyPlace();
|
||||
};
|
||||
|
||||
document.getElementById('closeButton').onclick = function () {
|
||||
MapEditor.closePlace();
|
||||
};
|
||||
|
||||
document.getElementById('deleteButton').onclick = function () {
|
||||
MapEditor.deletePlace();
|
||||
};
|
||||
|
||||
document.getElementById('jumpButton').onclick = function (e) {
|
||||
var coordinatesStr = document.getElementById("jumpCoordinates").value;
|
||||
var coordinates = Util.extractCoordinates(coordinatesStr);
|
||||
|
||||
if (coordinates.valid) {
|
||||
MapEditor.map.placeMarker(coordinates.latlng);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('jumpCoordinates').onkeyup = function (e) {
|
||||
var coordinatesStr = document.getElementById("jumpCoordinates").value;
|
||||
var coordinates = Util.extractCoordinates(coordinatesStr);
|
||||
var jumpButton = document.getElementById("jumpButton");
|
||||
|
||||
if (coordinates.valid) {
|
||||
jumpButton.disabled = false;
|
||||
|
||||
if (e.key == 'Enter') {
|
||||
MapEditor.map.placeMarker(coordinates.latlng);
|
||||
}
|
||||
}
|
||||
else {
|
||||
jumpButton.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('mapSelector').onclick = function () {
|
||||
MapEditor.closePlace();
|
||||
MapEditor.map.hide();
|
||||
|
||||
if (MapEditor.map === GMapWrapper) {
|
||||
MapEditor.map = LMapWrapper;
|
||||
MapEditor.map.init('lmap', places);
|
||||
} else {
|
||||
MapEditor.map = GMapWrapper;
|
||||
MapEditor.map.init('gmap', places);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('streetViewCoverSelector').onclick = function () {
|
||||
MapEditor.map.toggleStreetViewCover();
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,267 @@
|
||||
var MapGuesser = {
|
||||
isSecure: window.location.protocol === 'https:',
|
||||
cookiesAgreed: false,
|
||||
sessionAvailableHooks: {},
|
||||
|
||||
initGoogleAnalitics: function () {
|
||||
if (typeof GOOGLE_ANALITICS_ID === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Global site tag (gtag.js) - Google Analytics
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ANALITICS_ID;
|
||||
script.async = true;
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
gtag('config', GOOGLE_ANALITICS_ID);
|
||||
},
|
||||
|
||||
agreeCookies: function () {
|
||||
if (MapGuesser.cookiesAgreed) {
|
||||
return;
|
||||
}
|
||||
|
||||
var expirationDate = new Date(new Date().getTime() + 20 * 365 * 24 * 60 * 60 * 1000).toUTCString();
|
||||
document.cookie = 'COOKIES_CONSENT=1; expires=' + expirationDate + '; path=/';
|
||||
|
||||
MapGuesser.initGoogleAnalitics();
|
||||
MapGuesser.httpRequest('GET', '/startSession.json', function () {
|
||||
ANTI_CSRF_TOKEN = this.response.antiCsrfToken;
|
||||
|
||||
for (var hookId in MapGuesser.sessionAvailableHooks) {
|
||||
if (!MapGuesser.sessionAvailableHooks.hasOwnProperty(hookId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MapGuesser.sessionAvailableHooks[hookId]();
|
||||
}
|
||||
});
|
||||
|
||||
MapGuesser.cookiesAgreed = true;
|
||||
},
|
||||
|
||||
httpRequest: function (method, url, callback, data) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onload = callback;
|
||||
|
||||
xhr.open(method, url, true);
|
||||
|
||||
xhr.responseType = 'json';
|
||||
|
||||
if (method === 'POST') {
|
||||
if (typeof data === 'undefined') {
|
||||
data = new FormData();
|
||||
}
|
||||
|
||||
data.append('anti_csrf_token', ANTI_CSRF_TOKEN);
|
||||
|
||||
xhr.send(data);
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
},
|
||||
|
||||
setOnsubmitForForm: function (form) {
|
||||
form.onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
var formData = new FormData(form);
|
||||
var formError = form.getElementsByClassName('formError')[0];
|
||||
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
|
||||
|
||||
MapGuesser.httpRequest('POST', form.action, function () {
|
||||
if (!pageLeaveOnSuccess) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
if (this.response.error) {
|
||||
if (pageLeaveOnSuccess) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
formError.style.display = 'block';
|
||||
formError.innerHTML = this.response.error.errorText;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.response.redirect) {
|
||||
window.location.replace(this.response.redirect.target);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pageLeaveOnSuccess) {
|
||||
formError.style.display = 'none';
|
||||
form.reset();
|
||||
} else {
|
||||
if (form.dataset.redirectOnSuccess) {
|
||||
window.location.replace(form.dataset.redirectOnSuccess);
|
||||
} else if (form.dataset.reloadOnSuccess) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}, formData);
|
||||
}
|
||||
},
|
||||
|
||||
showModal: function (id) {
|
||||
document.getElementById(id).style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
},
|
||||
|
||||
showModalWithContent: function (title, body, extraButtons) {
|
||||
if (typeof extraButtons === 'undefined') {
|
||||
extraButtons = [];
|
||||
}
|
||||
|
||||
MapGuesser.showModal('modal');
|
||||
|
||||
document.getElementById('modalTitle').textContent = title;
|
||||
|
||||
if (typeof body === 'object') {
|
||||
document.getElementById('modalText').appendChild(body);
|
||||
} else {
|
||||
document.getElementById('modalText').textContent = body;
|
||||
}
|
||||
|
||||
var buttons = document.getElementById('modalButtons');
|
||||
buttons.textContent = '';
|
||||
|
||||
for (var i = 0; i < extraButtons.length; i++) {
|
||||
var extraButton = extraButtons[i];
|
||||
var button = document.createElement(extraButton.type);
|
||||
|
||||
if (extraButton.type === 'a') {
|
||||
button.classList.add('button');
|
||||
}
|
||||
|
||||
for (var i = 0; i < extraButton.classNames.length; i++) {
|
||||
button.classList.add(extraButton.classNames[i]);
|
||||
}
|
||||
|
||||
button.classList.add('marginTop');
|
||||
button.classList.add('marginRight');
|
||||
button.textContent = extraButton.text;
|
||||
|
||||
if (extraButton.type === 'a') {
|
||||
button.href = extraButton.href;
|
||||
} else if (extraButton.type === 'button') {
|
||||
button.onclick = extraButton.onclick;
|
||||
}
|
||||
|
||||
buttons.appendChild(button);
|
||||
}
|
||||
|
||||
var closeButton = document.createElement('button');
|
||||
|
||||
closeButton.classList.add('gray');
|
||||
closeButton.classList.add('marginTop');
|
||||
closeButton.textContent = 'Close';
|
||||
closeButton.onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
buttons.appendChild(closeButton);
|
||||
},
|
||||
|
||||
hideModal: function () {
|
||||
var modals = document.getElementsByClassName('modal');
|
||||
|
||||
for (var i = 0; i < modals.length; i++) {
|
||||
modals[i].style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
},
|
||||
|
||||
observeInput: function (input, buttonToToggle) {
|
||||
if (input.defaultValue !== input.value) {
|
||||
buttonToToggle.disabled = false;
|
||||
} else {
|
||||
buttonToToggle.disabled = true;
|
||||
}
|
||||
},
|
||||
|
||||
observeInputsInForm: function (form, observedInputs) {
|
||||
for (var i = 0; i < observedInputs.length; i++) {
|
||||
var input = form.elements[observedInputs[i]];
|
||||
|
||||
switch (input.tagName) {
|
||||
case 'INPUT':
|
||||
case 'TEXTAREA':
|
||||
input.oninput = function () {
|
||||
MapGuesser.observeInput(this, form.elements.submit);
|
||||
};
|
||||
break;
|
||||
case 'SELECT':
|
||||
input.onchange = function () {
|
||||
MapGuesser.observeInput(this, form.elements.submit);
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
form.onreset = function () {
|
||||
form.elements.submit.disabled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(function () {
|
||||
var anchors = document.getElementsByTagName('a');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
var a = anchors[i];
|
||||
if (a.href !== 'javascript:;' && a.target !== '_blank') {
|
||||
a.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var forms = document.getElementsByTagName('form');
|
||||
for (var i = 0; i < forms.length; i++) {
|
||||
var form = forms[i];
|
||||
|
||||
if (form.dataset.noSubmit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MapGuesser.setOnsubmitForForm(form);
|
||||
|
||||
if (form.dataset.observeInputs) {
|
||||
MapGuesser.observeInputsInForm(form, form.dataset.observeInputs.split(','));
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('cover').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
if (COOKIES_CONSENT) {
|
||||
MapGuesser.initGoogleAnalitics();
|
||||
} else {
|
||||
// we don't want user to agree cookies when clicking on the notice itself
|
||||
document.getElementById('cookiesNotice').onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
document.getElementById('agreeCookiesButton').onclick = function () {
|
||||
MapGuesser.agreeCookies();
|
||||
|
||||
document.getElementById('cookiesNotice').style.display = 'none';
|
||||
};
|
||||
|
||||
window.onclick = function () {
|
||||
MapGuesser.agreeCookies();
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,112 @@
|
||||
(function () {
|
||||
var Maps = {
|
||||
descriptionDivs: null,
|
||||
|
||||
addStaticMaps: function () {
|
||||
var imgContainers = document.getElementById('mapContainer').getElementsByClassName('imgContainer');
|
||||
for (var i = 0; i < imgContainers.length; i++) {
|
||||
var imgContainer = imgContainers[i];
|
||||
|
||||
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
|
||||
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' +
|
||||
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
|
||||
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng +
|
||||
'&key=' + GOOGLE_MAPS_JS_API_KEY;
|
||||
|
||||
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
|
||||
}
|
||||
},
|
||||
|
||||
initializeDescriptionDivs: function () {
|
||||
Maps.descriptionDivs = document.getElementById('mapContainer').getElementsByClassName('description');
|
||||
|
||||
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
|
||||
var description = Maps.descriptionDivs[i];
|
||||
var boundingClientRect = description.getBoundingClientRect();
|
||||
|
||||
description.defaultHeight = boundingClientRect.height;
|
||||
}
|
||||
},
|
||||
|
||||
calculateDescriptionDivHeights: function () {
|
||||
var currentY;
|
||||
var rows = [];
|
||||
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
|
||||
var description = Maps.descriptionDivs[i];
|
||||
var boundingClientRect = description.getBoundingClientRect();
|
||||
|
||||
if (currentY !== boundingClientRect.y) {
|
||||
rows.push([]);
|
||||
}
|
||||
|
||||
rows[rows.length - 1].push(description);
|
||||
|
||||
currentY = boundingClientRect.y;
|
||||
}
|
||||
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
|
||||
var maxHeight = 0;
|
||||
for (var j = 0; j < row.length; j++) {
|
||||
var description = row[j];
|
||||
|
||||
if (description.defaultHeight > maxHeight) {
|
||||
maxHeight = description.defaultHeight;
|
||||
}
|
||||
}
|
||||
|
||||
for (var j = 0; j < row.length; j++) {
|
||||
var description = row[j];
|
||||
|
||||
description.style.height = maxHeight + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Maps.addStaticMaps();
|
||||
|
||||
Maps.initializeDescriptionDivs();
|
||||
Maps.calculateDescriptionDivHeights();
|
||||
|
||||
window.onresize = function () {
|
||||
Maps.calculateDescriptionDivHeights();
|
||||
};
|
||||
|
||||
document.getElementById('multiForm').onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var roomId = this.elements.roomId.value;
|
||||
if (roomId.length !== 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = '/multiGame/' + this.elements.roomId.value;
|
||||
};
|
||||
|
||||
document.getElementById('multiButton').onclick = function () {
|
||||
MapGuesser.showModal('multi');
|
||||
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
|
||||
document.getElementById('multiForm').elements.roomId.select();
|
||||
}
|
||||
|
||||
document.getElementById('closePlayModeButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
document.getElementById('closeMultiButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var button = buttons[i];
|
||||
|
||||
button.onclick = function () {
|
||||
MapGuesser.showModal('playMode');
|
||||
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
|
||||
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
(function () {
|
||||
Maps = {
|
||||
deleteMap: function (mapId, mapName) {
|
||||
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
|
||||
type: 'button',
|
||||
classNames: ['red'],
|
||||
text: 'Delete',
|
||||
onclick: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
|
||||
if (this.response.error) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
//TODO: handle this error
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var button = buttons[i];
|
||||
|
||||
button.onclick = function () {
|
||||
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"leaflet": "^1.6.0",
|
||||
"leaflet.markercluster": "^1.4.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
leaflet.markercluster@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5"
|
||||
integrity sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==
|
||||
|
||||
leaflet@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
|
||||
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
|
||||
exit(1)
|
||||
|
||||
REPO = os.path.abspath(sys.argv[1])
|
||||
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
|
||||
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
|
||||
|
||||
class Worktree:
|
||||
def __init__(self, path, branch, revision, version):
|
||||
self.path = path
|
||||
self.branch = branch
|
||||
self.revision = revision
|
||||
self.version = version
|
||||
self.newRevision = None
|
||||
self.newVersion = None
|
||||
|
||||
def getDataForWorktrees():
|
||||
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
|
||||
blocks = ret.split("\n\n")
|
||||
|
||||
worktrees = []
|
||||
|
||||
for block in blocks:
|
||||
matches = re.search(WORKTREE_REGEX, block)
|
||||
|
||||
if matches:
|
||||
path = matches.group(1)
|
||||
revision = matches.group(2)
|
||||
branch = matches.group(3)
|
||||
version = getVersion(revision)
|
||||
|
||||
worktrees.append(Worktree(path, branch, revision, version))
|
||||
|
||||
return worktrees
|
||||
|
||||
def findWorktree(path):
|
||||
for worktree in worktrees:
|
||||
if worktree.path == path:
|
||||
return worktree
|
||||
|
||||
return None
|
||||
|
||||
def getVersion(branch):
|
||||
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
|
||||
|
||||
def getRevisionForRef(ref):
|
||||
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
||||
|
||||
def getLatestReleaseTag():
|
||||
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
|
||||
|
||||
for line in process.stdout:
|
||||
tag = line.decode().rstrip()
|
||||
|
||||
if isTagVerified(tag):
|
||||
return tag
|
||||
|
||||
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
|
||||
|
||||
raise Exception("No verified 'Release*' tag found!")
|
||||
|
||||
def isTagVerified(tag):
|
||||
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
|
||||
|
||||
return process.returncode == 0
|
||||
|
||||
def updateRepoFromRemote():
|
||||
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
|
||||
|
||||
def checkoutWorktree(worktreePath, ref):
|
||||
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
||||
|
||||
def cleanWorktree(worktreePath):
|
||||
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
|
||||
|
||||
def updateAppInWorktree(worktreePath):
|
||||
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
|
||||
|
||||
def updateAppVersionInWorktree(worktreePath):
|
||||
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
|
||||
|
||||
worktrees = getDataForWorktrees()
|
||||
|
||||
updateRepoFromRemote()
|
||||
|
||||
print("Repo is updated from origin")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
|
||||
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
|
||||
|
||||
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
|
||||
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
|
||||
|
||||
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
|
||||
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
|
||||
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
|
||||
|
||||
if developmentWorktree.revision != developmentWorktree.newRevision:
|
||||
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
|
||||
print("----------------------------------------------")
|
||||
|
||||
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
|
||||
cleanWorktree(developmentWorktree.path)
|
||||
|
||||
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
|
||||
|
||||
updateAppInWorktree(developmentWorktree.path)
|
||||
updateAppVersionInWorktree(developmentWorktree.path)
|
||||
|
||||
print("MapGuesser is updated in " + developmentWorktree.path)
|
||||
elif developmentWorktree.version != developmentWorktree.newVersion:
|
||||
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
|
||||
|
||||
updateAppVersionInWorktree(developmentWorktree.path)
|
||||
|
||||
print("MapGuesser version is updated in " + developmentWorktree.path)
|
||||
else:
|
||||
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
|
||||
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
|
||||
|
||||
productionWorktree.newVersion = getLatestReleaseTag()
|
||||
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
|
||||
|
||||
print("PRODUCTION (" + productionWorktree.path + ")")
|
||||
print(productionWorktree.revision + " = " + productionWorktree.version)
|
||||
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
|
||||
|
||||
if productionWorktree.revision != productionWorktree.newRevision:
|
||||
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
|
||||
|
||||
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
|
||||
cleanWorktree(productionWorktree.path)
|
||||
|
||||
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
|
||||
|
||||
updateAppInWorktree(productionWorktree.path)
|
||||
updateAppVersionInWorktree(productionWorktree.path)
|
||||
|
||||
print("MapGuesser is updated in " + productionWorktree.path)
|
||||
else:
|
||||
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
BRANCH_NAME=$(git symbolic-ref --short HEAD)
|
||||
BRANCH_PATTERN="^(bugfix|feature|hotfix)\/([A-Z]+-[0-9]+).*"
|
||||
|
||||
if [[ "${BRANCH_NAME}" =~ $BRANCH_PATTERN ]]; then
|
||||
TICKET_ID=$(echo $BRANCH_NAME | sed -E "s@$BRANCH_PATTERN@\\2@")
|
||||
|
||||
COMMIT_MESSAGE=$(head -n 1 $1)
|
||||
COMMIT_MESSAGE_REGEX="^$TICKET_ID .*"
|
||||
|
||||
if [[ ! "${COMMIT_MESSAGE}" =~ $COMMIT_MESSAGE_REGEX ]]; then
|
||||
sed -i.bak -e "1s/^/$TICKET_ID /" $1
|
||||
fi
|
||||
fi
|
||||
@@ -9,14 +9,27 @@ if [ -f ${ROOT_DIR}/installed ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing MapGuesser DB..."
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Installing MapGuesser DB..."
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
echo "Linking view files..."
|
||||
(cd ${ROOT_DIR} && ./mapg view:link)
|
||||
else
|
||||
echo "Creating the first user..."
|
||||
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
|
||||
fi
|
||||
|
||||
touch ${ROOT_DIR}/installed
|
||||
@@ -4,6 +4,8 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
uglifyjs ${ROOT_DIR}/public/static/js/game.js -c -m -o ${ROOT_DIR}/public/static/js/game.js
|
||||
cleancss ${ROOT_DIR}/public/static/css/mapguesser.css -o ${ROOT_DIR}/public/static/css/mapguesser.css
|
||||
svgo ${ROOT_DIR}/public/static/img/loading.svg -o ${ROOT_DIR}/public/static/img/loading.svg
|
||||
find ${ROOT_DIR}/public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
|
||||
|
||||
find ${ROOT_DIR}/public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
|
||||
|
||||
find ${ROOT_DIR}/public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Usage: $0 <sql_file>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SQL_FILE=$1
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "This script can only be used in DEV mode!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running SQL on DB..."
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${SQL_FILE}
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
cd ${ROOT_DIR}
|
||||
|
||||
echo "Updating version info..."
|
||||
|
||||
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
|
||||
REVISION=$(git rev-parse --short HEAD)
|
||||
REVISION_DATE=$(git show -s --format=%aI HEAD)
|
||||
|
||||
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
|
||||
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
|
||||
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
|
||||
@@ -5,10 +5,25 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
echo "Installing Composer packages..."
|
||||
(cd ${ROOT_DIR} && composer install)
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
(cd ${ROOT_DIR} && composer install --no-dev)
|
||||
else
|
||||
(cd ${ROOT_DIR} && composer install --dev)
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
echo "Linking view files..."
|
||||
(cd ${ROOT_DIR} && ./mapg view:link)
|
||||
fi
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('user:add')
|
||||
->setDescription('Adding of user.')
|
||||
->addArgument('email', InputArgument::REQUIRED, 'Email of user')
|
||||
->addArgument('password', InputArgument::REQUIRED, 'Password of user')
|
||||
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEmail($input->getArgument('email'));
|
||||
$user->setPlainPassword($input->getArgument('password'));
|
||||
$user->setActive(true);
|
||||
$user->setCreatedDate(new DateTime());
|
||||
|
||||
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
|
||||
$user->setType($input->getArgument('type'));
|
||||
}
|
||||
|
||||
try {
|
||||
$pdm = new PersistentDataManager();
|
||||
$pdm->saveToDb($user);
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Adding user failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>User was successfully added!</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use FilesystemIterator;
|
||||
use MapGuesser\View\Linker;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LinkViewCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('view:link')
|
||||
->setDescription('Linking of views.')
|
||||
->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$views = [];
|
||||
|
||||
$view = $input->getArgument('view');
|
||||
|
||||
if ($view !== null) {
|
||||
$views[] = $view;
|
||||
} else {
|
||||
$folder = ROOT . '/views';
|
||||
$folderLength = strlen($folder) + 1;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
|
||||
|
||||
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$views[] = $view;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($views as $view) {
|
||||
$generator = new Linker($view);
|
||||
$generator->generate();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Linking view(s) failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>View(s) successfully linked!</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MaintainDatabaseCommand extends Command
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('db:maintain')
|
||||
->setDescription('Maintain database.');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
$this->deleteInactiveExpiredUsers();
|
||||
$this->deleteExpiredPasswordResetters();
|
||||
$this->deleteExpiredRooms();
|
||||
$this->deleteExpiredSessions();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Maintenance failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>Maintenance was successful!</info>');
|
||||
$output->writeln('');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function deleteInactiveExpiredUsers(): void
|
||||
{
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
|
||||
//TODO: these can be in some wrapper class
|
||||
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
|
||||
if ($userConfirmation !== null) {
|
||||
$this->pdm->deleteFromDb($userConfirmation);
|
||||
}
|
||||
|
||||
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
|
||||
if ($userPasswordResetter !== null) {
|
||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||
}
|
||||
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
}
|
||||
|
||||
private function deleteExpiredPasswordResetters(): void
|
||||
{
|
||||
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
|
||||
$this->pdm->deleteFromDb($passwordResetter);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredRooms(): void
|
||||
{
|
||||
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
|
||||
$this->pdm->deleteFromDb($multiRoom);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredSessions(): void
|
||||
{
|
||||
//TODO: model may be used for sessions too
|
||||
$select = new Select(\Container::$dbConnection, 'sessions');
|
||||
$select->columns(['id']);
|
||||
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$modify = new Modify(\Container::$dbConnection, 'sessions');
|
||||
$modify->setId($session['id']);
|
||||
$modify->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MigrateDatabaseCommand extends Command
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('db:migrate')
|
||||
->setDescription('Migration of database changes.');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$db = \Container::$dbConnection;
|
||||
|
||||
$db->startTransaction();
|
||||
|
||||
$success = [];
|
||||
try {
|
||||
foreach ($this->readDir('structure') as $file) {
|
||||
$db->multiQuery(file_get_contents($file));
|
||||
|
||||
$success[] = $this->saveToDB($file, 'structure');
|
||||
}
|
||||
|
||||
foreach ($this->readDir('data') as $file) {
|
||||
require $file;
|
||||
|
||||
$success[] = $this->saveToDB($file, 'data');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$db->rollback();
|
||||
|
||||
$output->writeln('<error>Migration failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
$output->writeln('<info>Migration was successful!</info>');
|
||||
$output->writeln('');
|
||||
|
||||
if (count($success) > 0) {
|
||||
foreach ($success as $migration) {
|
||||
$output->writeln($migration);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function readDir(string $type): array
|
||||
{
|
||||
$done = [];
|
||||
|
||||
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
|
||||
AND table_name = \'migrations\';')
|
||||
->fetch(IResultSet::FETCH_NUM)[0];
|
||||
|
||||
if ($migrationTableExists != 0) {
|
||||
$select = new Select(\Container::$dbConnection, 'migrations');
|
||||
$select->columns(['migration']);
|
||||
$select->where('type', '=', $type);
|
||||
$select->orderBy('migration');
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$done[] = $migration['migration'];
|
||||
}
|
||||
}
|
||||
|
||||
$path = ROOT . '/database/migrations/' . $type;
|
||||
$dir = opendir($path);
|
||||
|
||||
if ($dir === false) {
|
||||
throw new \Exception('Cannot open dir: ' . $path);
|
||||
}
|
||||
|
||||
$files = [];
|
||||
while ($file = readdir($dir)) {
|
||||
$filePath = $path . '/' . $file;
|
||||
|
||||
if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[] = $filePath;
|
||||
}
|
||||
|
||||
natsort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function saveToDB(string $file, string $type): string
|
||||
{
|
||||
$baseName = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'migrations');
|
||||
$modify->set('migration', $baseName);
|
||||
$modify->set('type', $type);
|
||||
$modify->save();
|
||||
|
||||
return $baseName . ' (' . $type . ')';
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,174 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use DateTime;
|
||||
use Faker\Factory;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\Multi\MultiConnector;
|
||||
use MapGuesser\PersistentData\Model\MultiRoom;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Response\Redirect;
|
||||
|
||||
class GameController
|
||||
{
|
||||
public function getGame(array $parameters): IContent
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
$data = $this->prepareGame($mapId);
|
||||
return new HtmlContent('game', $data);
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->mapRepository = new MapRepository();
|
||||
}
|
||||
|
||||
public function getGameJson(array $parameters): IContent
|
||||
public function getGame(): IContent
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
$data = $this->prepareGame($mapId);
|
||||
return new JsonContent($data);
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
return new HtmlContent('game', ['mapId' => $mapId]);
|
||||
}
|
||||
|
||||
private function prepareGame(int $mapId)
|
||||
public function getNewMultiGame(): IRedirect
|
||||
{
|
||||
$bounds = $this->getMapBounds($mapId);
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$roomId = bin2hex(random_bytes(3));
|
||||
$token = $this->getMultiToken($roomId);
|
||||
|
||||
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
|
||||
$_SESSION['state'] = [
|
||||
$room = new MultiRoom();
|
||||
$room->setRoomId($roomId);
|
||||
$room->setStateArray([
|
||||
'mapId' => $mapId,
|
||||
'area' => $map->getArea(),
|
||||
'rounds' => [],
|
||||
'currentRound' => -1
|
||||
]);
|
||||
$room->setMembersArray(['owner' => $token, 'all' => []]);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($room);
|
||||
|
||||
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
|
||||
|
||||
return new Redirect(
|
||||
\Container::$routeCollection
|
||||
->getRoute('multiGame')
|
||||
->generateLink(['roomId' => $roomId]),
|
||||
IRedirect::TEMPORARY
|
||||
);
|
||||
}
|
||||
|
||||
public function getMultiGame(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
|
||||
return new HtmlContent('game', ['roomId' => $roomId]);
|
||||
}
|
||||
|
||||
public function prepareGame(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$session->set('state', [
|
||||
'mapId' => $mapId,
|
||||
'area' => $bounds->calculateApproximateArea(),
|
||||
'rounds' => []
|
||||
];
|
||||
'area' => $map->getArea(),
|
||||
'rounds' => [],
|
||||
'currentRound' => -1
|
||||
]);
|
||||
} else { // update the area of the map in the session in any case
|
||||
$state['area'] = $map->getArea();
|
||||
$session->set('state', $state);
|
||||
}
|
||||
|
||||
return ['mapId' => $mapId, 'bounds' => $bounds->toArray()];
|
||||
return new JsonContent([
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'bounds' => $map->getBounds()->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getMapBounds(int $mapId): Bounds
|
||||
public function prepareMultiGame(): IContent
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
$select->whereId($mapId);
|
||||
$roomId = $this->request->query('roomId');
|
||||
$userName = $this->request->post('userName');
|
||||
if (empty($userName)) {
|
||||
$faker = Factory::create();
|
||||
$userName = $faker->userName;
|
||||
}
|
||||
|
||||
$map = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
if(!isset($room)) {
|
||||
return new JsonContent(['error' => 'game_not_found']);
|
||||
}
|
||||
|
||||
return $bounds;
|
||||
$state = $room->getStateArray();
|
||||
$map = $this->mapRepository->getById($state['mapId']);
|
||||
$token = $this->getMultiToken($roomId);
|
||||
|
||||
$members = $room->getMembersArray();
|
||||
|
||||
if (!in_array($token, $members['all'])) {
|
||||
if ($state['currentRound'] >= 0) {
|
||||
return new JsonContent(['error' => 'game_already_started']);
|
||||
}
|
||||
|
||||
$members['all'][] = $token;
|
||||
}
|
||||
|
||||
$room->setMembersArray($members);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($room);
|
||||
|
||||
$this->multiConnector->sendMessage('join_room', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $token,
|
||||
'userName' => $userName
|
||||
]);
|
||||
|
||||
return new JsonContent([
|
||||
'roomId' => $roomId,
|
||||
'token' => $token,
|
||||
'owner' => $members['owner'] == $token,
|
||||
'mapId' => $state['mapId'],
|
||||
'mapName' => $map->getName(),
|
||||
'bounds' => $map->getBounds()->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getMultiToken(string $roomId): string
|
||||
{
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
$session->set('multiState', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $token
|
||||
]);
|
||||
} else {
|
||||
$token = $multiState['token'];
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Multi\MultiConnector;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\PersistentData\Model\UserPlayedPlace;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
|
||||
class GameFlowController
|
||||
{
|
||||
const NUMBER_OF_ROUNDS = 5;
|
||||
const MAX_SCORE = 1000;
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
}
|
||||
|
||||
public function initialData(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
|
||||
$this->startNewGame($state, $mapId);
|
||||
$session->set('state', $state);
|
||||
}
|
||||
|
||||
$response = [];
|
||||
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$response['place'] = [
|
||||
'panoId' => $last['panoId'],
|
||||
'pov' => $last['pov']->toArray()
|
||||
];
|
||||
|
||||
$response['history'] = [];
|
||||
for ($i = 0; $i < $state['currentRound']; ++$i) {
|
||||
$round = $state['rounds'][$i];
|
||||
$response['history'][] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'result' => [
|
||||
'guessPosition' => $round['guessPosition']->toArray(),
|
||||
'distance' => $round['distance'],
|
||||
'score' => $round['score']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
public function multiInitialData(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
$members = $room->getMembersArray();
|
||||
|
||||
if ($members['owner'] !== $multiState['token']) {
|
||||
return new JsonContent(['error' => 'not_owner_of_room']);
|
||||
}
|
||||
|
||||
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
|
||||
$this->startNewGame($state, $state['mapId']);
|
||||
$room->setStateArray($state);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
$this->pdm->saveToDb($room);
|
||||
}
|
||||
|
||||
$places = [];
|
||||
foreach ($state['rounds'] as $round) {
|
||||
$places[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'panoId' => $round['panoId'],
|
||||
'pov' => $round['pov']->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
|
||||
|
||||
return new JsonContent(['ok' => true]);
|
||||
}
|
||||
|
||||
public function guess(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
$last['distance'] = $result['distance'];
|
||||
$last['score'] = $result['score'];
|
||||
|
||||
$response = [
|
||||
'position' => $last['position']->toArray(),
|
||||
'result' => $result
|
||||
];
|
||||
|
||||
$state['rounds'][$state['currentRound']] = $last;
|
||||
$state['currentRound'] += 1;
|
||||
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
|
||||
$next = $state['rounds'][$state['currentRound']];
|
||||
|
||||
$response['place'] = [
|
||||
'panoId' => $next['panoId'],
|
||||
'pov' => $next['pov']->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
$session->set('state', $state);
|
||||
|
||||
$this->saveVisit($last);
|
||||
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
// save the selected place for the round in UserPlayedPlace
|
||||
private function saveVisit($last): void
|
||||
{
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
|
||||
if(isset($userId)) {
|
||||
$placeId = $last['placeId'];
|
||||
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
|
||||
if(!$userPlayedPlace) {
|
||||
$userPlayedPlace = new UserPlayedPlace();
|
||||
$userPlayedPlace->setUserId($userId);
|
||||
$userPlayedPlace->setPlaceId($placeId);
|
||||
} else {
|
||||
$userPlayedPlace->incrementOccurrences();
|
||||
}
|
||||
$userPlayedPlace->setLastTimeDate(new DateTime());
|
||||
$this->pdm->saveToDb($userPlayedPlace);
|
||||
}
|
||||
}
|
||||
|
||||
public function multiGuess(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
|
||||
|
||||
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $multiState['token'],
|
||||
'guessPosition' => $guessPosition->toArray(),
|
||||
'distance' => $result['distance'],
|
||||
'score' => $result['score']
|
||||
]);
|
||||
|
||||
if (isset($responseFromMulti['error'])) {
|
||||
return new JsonContent(['error' => $responseFromMulti['error']]);
|
||||
}
|
||||
|
||||
$response = [
|
||||
'position' => $last['position']->toArray(),
|
||||
'result' => $result,
|
||||
'allResults' => $responseFromMulti['allResults']
|
||||
];
|
||||
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
public function multiNextRound(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
|
||||
return new JsonContent(['error' => 'no_session_found']);
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
$state = $room->getStateArray();
|
||||
$members = $room->getMembersArray();
|
||||
|
||||
if ($members['owner'] !== $multiState['token']) {
|
||||
return new JsonContent(['error' => 'not_owner_of_room']);
|
||||
}
|
||||
|
||||
$state['currentRound'] += 1;
|
||||
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
|
||||
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
|
||||
}
|
||||
|
||||
$room->setStateArray($state);
|
||||
$room->setUpdatedDate(new DateTime());
|
||||
$this->pdm->saveToDb($room);
|
||||
|
||||
return new JsonContent(['ok' => true]);
|
||||
}
|
||||
|
||||
private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area)
|
||||
{
|
||||
$distance = $this->calculateDistance($realPosition, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $area);
|
||||
|
||||
return ['distance' => $distance, 'score' => $score];
|
||||
}
|
||||
|
||||
private function startNewGame(array &$state, int $mapId): void
|
||||
{
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
|
||||
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
|
||||
|
||||
$state['rounds'] = [];
|
||||
$state['currentRound'] = 0;
|
||||
|
||||
foreach ($places as $place) {
|
||||
$state['rounds'][] = [
|
||||
'placeId' => $place->getId(),
|
||||
'position' => $place->getPosition(),
|
||||
'panoId' => $place->getPanoIdCached(),
|
||||
'pov' => $place->getPov()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateDistance(Position $realPosition, Position $guessPosition): float
|
||||
{
|
||||
return $realPosition->calculateDistanceTo($guessPosition);
|
||||
}
|
||||
|
||||
private function calculateScore(float $distance, float $area): int
|
||||
{
|
||||
$goodness = 1.0 - ($distance / (sqrt($area) * 1000));
|
||||
|
||||
return (int) round(pow(static::MAX_SCORE, $goodness));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,29 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Response\Redirect;
|
||||
|
||||
class HomeController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getIndex(): IRedirect
|
||||
{
|
||||
return new Redirect([\Container::$routeCollection->getRoute('maps'), []], IRedirect::TEMPORARY);
|
||||
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function startSession(): IContent
|
||||
{
|
||||
// session starts with the request, this method just sends valid data to the client
|
||||
|
||||
return new JsonContent(['antiCsrfToken' => $this->request->session()->get('anti_csrf_token')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\Mailing\Mail;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserConfirmation;
|
||||
use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Response\Redirect;
|
||||
use MapGuesser\Util\JwtParser;
|
||||
|
||||
class LoginController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
}
|
||||
|
||||
public function getLoginForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
return new HtmlContent('login/login');
|
||||
}
|
||||
|
||||
public function getGoogleLoginRedirect(): IRedirect
|
||||
{
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
$this->request->session()->set('oauth_state', $state);
|
||||
$this->request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(),
|
||||
$nonce
|
||||
);
|
||||
|
||||
return new Redirect($url, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function getSignupForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
if ($this->request->session()->has('tmp_user_data')) {
|
||||
$tmpUserData = $this->request->session()->get('tmp_user_data');
|
||||
|
||||
$data = ['email' => $tmpUserData['email']];
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
return new HtmlContent('login/signup', $data);
|
||||
}
|
||||
|
||||
public function getSignupSuccess(): IContent
|
||||
{
|
||||
return new HtmlContent('login/signup_success');
|
||||
}
|
||||
|
||||
public function getSignupWithGoogleForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
if (!$this->request->session()->has('google_user_data')) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('login-google')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$userData = $this->request->session()->get('google_user_data');
|
||||
|
||||
$user = $this->userRepository->getByEmail($userData['email']);
|
||||
|
||||
return new HtmlContent('login/google_signup', ['found' => $user !== null, 'email' => $userData['email']]);
|
||||
}
|
||||
|
||||
public function getRequestPasswordResetForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]);
|
||||
}
|
||||
|
||||
public function getRequestPasswordResetSuccess(): IContent
|
||||
{
|
||||
return new HtmlContent('login/password_reset_request_success');
|
||||
}
|
||||
|
||||
public function getResetPasswordForm()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$token = $this->request->query('token');
|
||||
$resetter = $this->userPasswordResetterRepository->getByToken($token);
|
||||
|
||||
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
|
||||
return new HtmlContent('login/reset_password', ['success' => false]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getById($resetter->getUserId());
|
||||
|
||||
return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail()]);
|
||||
}
|
||||
|
||||
public function login(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
|
||||
if ($user === null) {
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$tmpUser = new User();
|
||||
$tmpUser->setPlainPassword($this->request->post('password'));
|
||||
|
||||
$this->request->session()->set('tmp_user_data', [
|
||||
'email' => $this->request->post('email'),
|
||||
'password_hashed' => $tmpUser->getPassword()
|
||||
]);
|
||||
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('signup')->generateLink()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->getActive()) {
|
||||
$this->resendConfirmationEmail($user);
|
||||
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'User found with the given email address, but the account is not activated. ' .
|
||||
'Please check your email and click on the activation link!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->checkPassword($this->request->post('password'))) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is wrong. You can <a href="/password/requestReset?email=' .
|
||||
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function loginWithGoogle()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
$this->request->query('code'),
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink()
|
||||
);
|
||||
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$idToken = $jwtParser->getPayload();
|
||||
|
||||
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
if (!$idToken['email_verified']) {
|
||||
return new HtmlContent('login/google_login');
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByGoogleSub($idToken['sub']);
|
||||
|
||||
if ($user === null) {
|
||||
$this->request->session()->set('google_user_data', ['sub' => $idToken['sub'], 'email' => $idToken['email']]);
|
||||
|
||||
return new Redirect(\Container::$routeCollection->getRoute('signup-google')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function logout(): IRedirect
|
||||
{
|
||||
$this->request->setUser(null);
|
||||
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function signup(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent(['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()]]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
|
||||
if ($user !== null) {
|
||||
if ($user->getActive()) {
|
||||
if (!$user->checkPassword($this->request->post('password'))) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'There is a user already registered with the given email address, ' .
|
||||
'but the given password is wrong. You can <a href="/password/requestReset?email=' .
|
||||
urlencode($user->getEmail()) . '" title="Request password reset">request password reset</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
$data = ['redirect' => ['target' => '/' . \Container::$routeCollection->getRoute('index')->generateLink()]];
|
||||
} else {
|
||||
$data = [
|
||||
'error' => [
|
||||
'errorText' => 'There is a user already registered with the given email address. ' .
|
||||
'Please check your email and click on the activation link!'
|
||||
]
|
||||
];
|
||||
}
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
|
||||
}
|
||||
|
||||
if ($this->request->session()->has('tmp_user_data')) {
|
||||
$tmpUserData = $this->request->session()->get('tmp_user_data');
|
||||
|
||||
$tmpUser = new User();
|
||||
$tmpUser->setPassword($tmpUserData['password_hashed']);
|
||||
|
||||
if (!$tmpUser->checkPassword($this->request->post('password'))) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
} else {
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($this->request->post('email'));
|
||||
$user->setPlainPassword($this->request->post('password'));
|
||||
$user->setCreatedDate(new DateTime());
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
$token = bin2hex(random_bytes(16));
|
||||
|
||||
$confirmation = new UserConfirmation();
|
||||
$confirmation->setUser($user);
|
||||
$confirmation->setToken($token);
|
||||
$confirmation->setLastSentDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($confirmation);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->sendConfirmationEmail($user->getEmail(), $token, $user->getCreatedDate());
|
||||
|
||||
$this->request->session()->delete('tmp_user_data');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function signupWithGoogle(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
$userData = $this->request->session()->get('google_user_data');
|
||||
|
||||
$user = $this->userRepository->getByEmail($userData['email']);
|
||||
|
||||
if ($user === null) {
|
||||
$sendWelcomeEmail = true;
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($userData['email']);
|
||||
$user->setCreatedDate(new DateTime());
|
||||
} else {
|
||||
$sendWelcomeEmail = false;
|
||||
}
|
||||
|
||||
$user->setActive(true);
|
||||
$user->setGoogleSub($userData['sub']);
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
if ($sendWelcomeEmail) {
|
||||
$this->sendWelcomeEmail($user->getEmail());
|
||||
}
|
||||
|
||||
$this->request->session()->delete('google_user_data');
|
||||
$this->request->setUser($user);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function resetSignup(): IContent
|
||||
{
|
||||
$this->request->session()->delete('tmp_user_data');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function resetGoogleSignup(): IContent
|
||||
{
|
||||
$this->request->session()->delete('google_user_data');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function activate()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
|
||||
|
||||
if ($confirmation === null) {
|
||||
return new HtmlContent('login/activate');
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->deleteFromDb($confirmation);
|
||||
|
||||
$user = $this->userRepository->getById($confirmation->getUserId());
|
||||
$user->setActive(true);
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function cancel()
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$confirmation = $this->userConfirmationRepository->getByToken(substr($this->request->query('token'), 0, 32));
|
||||
|
||||
if ($confirmation === null) {
|
||||
return new HtmlContent('login/cancel', ['success' => false]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->deleteFromDb($confirmation);
|
||||
|
||||
$user = $this->userRepository->getById($confirmation->getUserId());
|
||||
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
return new HtmlContent('login/cancel', ['success' => true]);
|
||||
}
|
||||
|
||||
public function requestPasswordReset(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
|
||||
if ($user === null) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'No user found with the given email address. You can <a href="/signup" title="Sign up">sign up</a>!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user->getActive()) {
|
||||
$this->resendConfirmationEmail($user);
|
||||
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'User found with the given email address, but the account is not activated. ' .
|
||||
'Please check your email and click on the activation link!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$existingResetter = $this->userPasswordResetterRepository->getByUser($user);
|
||||
|
||||
if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(16));
|
||||
$expires = new DateTime('+1 hour');
|
||||
|
||||
$passwordResetter = new UserPasswordResetter();
|
||||
$passwordResetter->setUser($user);
|
||||
$passwordResetter->setToken($token);
|
||||
$passwordResetter->setExpiresDate($expires);
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
if ($existingResetter !== null) {
|
||||
$this->pdm->deleteFromDb($existingResetter);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($passwordResetter);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->sendPasswordResetEmail($user->getEmail(), $token, $expires);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
|
||||
public function resetPassword(): IContent
|
||||
{
|
||||
if ($this->request->user() !== null) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('home')->generateLink()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $this->request->query('token');
|
||||
$resetter = $this->userPasswordResetterRepository->getByToken($token);
|
||||
|
||||
if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) {
|
||||
return new JsonContent([
|
||||
'redirect' => [
|
||||
'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token])
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (strlen($this->request->post('password')) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password') !== $this->request->post('password_confirm')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->pdm->deleteFromDb($resetter);
|
||||
|
||||
$user = $this->userRepository->getById($resetter->getUserId());
|
||||
$user->setPlainPassword($this->request->post('password'));
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->request->setUser($user);
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function sendConfirmationEmail(string $email, string $token, DateTime $created): void
|
||||
{
|
||||
$mail = new Mail();
|
||||
$mail->addRecipient($email);
|
||||
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME'] . ' - Activate your account');
|
||||
$mail->setBodyFromTemplate('signup', [
|
||||
'EMAIL' => $email,
|
||||
'ACTIVATE_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('signup.activate')->generateLink(['token' => $token]),
|
||||
'CANCEL_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('signup.cancel')->generateLink(['token' => $token]),
|
||||
'ACTIVATABLE_UNTIL' => (clone $created)->add(new DateInterval('P1D'))->format('Y-m-d H:i T')
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
|
||||
private function resendConfirmationEmail(User $user): bool
|
||||
{
|
||||
$confirmation = $this->userConfirmationRepository->getByUser($user);
|
||||
|
||||
if ($confirmation === null || (clone $confirmation->getLastSentDate())->add(new DateInterval('PT1H')) > new DateTime()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$confirmation->setLastSentDate(new DateTime());
|
||||
|
||||
$this->pdm->saveToDb($confirmation);
|
||||
|
||||
$this->sendConfirmationEmail($user->getEmail(), $confirmation->getToken(), $user->getCreatedDate());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function sendWelcomeEmail(string $email): void
|
||||
{
|
||||
$mail = new Mail();
|
||||
$mail->addRecipient($email);
|
||||
$mail->setSubject('Welcome to ' . $_ENV['APP_NAME']);
|
||||
$mail->setBodyFromTemplate('signup-noconfirm', [
|
||||
'EMAIL' => $email,
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
|
||||
private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void
|
||||
{
|
||||
$mail = new Mail();
|
||||
$mail->addRecipient($email);
|
||||
$mail->setSubject($_ENV['APP_NAME'] . ' - Password reset');
|
||||
$mail->setBodyFromTemplate('password-reset', [
|
||||
'EMAIL' => $email,
|
||||
'RESET_LINK' => $this->request->getBase() . '/' .
|
||||
\Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]),
|
||||
'EXPIRES' => $expires->format('Y-m-d H:i T')
|
||||
]);
|
||||
$mail->send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Util\Panorama\Pov;
|
||||
|
||||
class MapAdminController implements ISecured
|
||||
{
|
||||
private static string $unnamedMapName = '[unnamed map]';
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->mapRepository = new MapRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->request->user();
|
||||
|
||||
return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
|
||||
}
|
||||
|
||||
public function getMapEditor(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
if ($mapId) {
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$places = $this->getPlaces($map);
|
||||
} else {
|
||||
$map = new Map();
|
||||
$map->setName(self::$unnamedMapName);
|
||||
$places = [];
|
||||
}
|
||||
|
||||
return new HtmlContent('admin/map_editor', [
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
|
||||
'bounds' => $map->getBounds()->toArray(),
|
||||
'places' => &$places
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPlace(): IContent
|
||||
{
|
||||
$placeId = (int) $this->request->query('placeId');
|
||||
|
||||
$place = $this->placeRepository->getById($placeId);
|
||||
|
||||
return new JsonContent(['panoId' => $place->getFreshPanoId()]);
|
||||
}
|
||||
|
||||
public function saveMap(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
if ($mapId) {
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
} else {
|
||||
$map = new Map();
|
||||
$map->setName(self::$unnamedMapName);
|
||||
$this->pdm->saveToDb($map);
|
||||
}
|
||||
|
||||
if (isset($_POST['added'])) {
|
||||
$addedIds = [];
|
||||
foreach ($_POST['added'] as $placeRaw) {
|
||||
$placeRaw = json_decode($placeRaw, true);
|
||||
|
||||
$place = new Place();
|
||||
$place->setMap($map);
|
||||
$place->setLat((float) $placeRaw['lat']);
|
||||
$place->setLng((float) $placeRaw['lng']);
|
||||
$place->setPov(new Pov(
|
||||
(float) $placeRaw['pov']['heading'],
|
||||
(float) $placeRaw['pov']['pitch'],
|
||||
(float) $placeRaw['pov']['zoom']
|
||||
));
|
||||
|
||||
if ($placeRaw['panoId'] === -1) {
|
||||
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($place);
|
||||
|
||||
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
|
||||
}
|
||||
} else {
|
||||
$addedIds = [];
|
||||
}
|
||||
|
||||
if (isset($_POST['edited'])) {
|
||||
foreach ($_POST['edited'] as $placeRaw) {
|
||||
$placeRaw = json_decode($placeRaw, true);
|
||||
|
||||
$place = $this->placeRepository->getById((int) $placeRaw['id']);
|
||||
$place->setLat((float) $placeRaw['lat']);
|
||||
$place->setLng((float) $placeRaw['lng']);
|
||||
$place->setPov(new Pov(
|
||||
(float) $placeRaw['pov']['heading'],
|
||||
(float) $placeRaw['pov']['pitch'],
|
||||
(float) $placeRaw['pov']['zoom']
|
||||
));
|
||||
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
|
||||
|
||||
$this->pdm->saveToDb($place);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['deleted'])) {
|
||||
foreach ($_POST['deleted'] as $placeRaw) {
|
||||
$placeRaw = json_decode($placeRaw, true);
|
||||
|
||||
$place = $this->placeRepository->getById((int) $placeRaw['id']);
|
||||
|
||||
$this->deletePlace($place);
|
||||
}
|
||||
}
|
||||
|
||||
$mapBounds = $this->calculateMapBounds($map);
|
||||
|
||||
$map->setBounds($mapBounds);
|
||||
$map->setArea($mapBounds->calculateApproximateArea());
|
||||
|
||||
if (isset($_POST['name'])) {
|
||||
$map->setName($_POST['name'] ? $_POST['name'] : self::$unnamedMapName);
|
||||
}
|
||||
if (isset($_POST['description'])) {
|
||||
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($map);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
|
||||
}
|
||||
|
||||
public function deleteMap(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$this->deletePlaces($map);
|
||||
|
||||
$this->pdm->deleteFromDb($map);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function deletePlace(Place $place): void
|
||||
{
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($place);
|
||||
}
|
||||
|
||||
private function deletePlaces(Map $map): void
|
||||
{
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$this->deletePlace($place);
|
||||
}
|
||||
}
|
||||
|
||||
private function calculateMapBounds(Map $map): Bounds
|
||||
{
|
||||
$bounds = new Bounds();
|
||||
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$bounds->extend($place->getPosition());
|
||||
}
|
||||
|
||||
return $bounds;
|
||||
}
|
||||
|
||||
private function &getPlaces(Map $map): array
|
||||
{
|
||||
$places = [];
|
||||
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$noPano = $place->getPanoIdCachedTimestampDate() !== null && $place->getPanoIdCached() === null;
|
||||
|
||||
$placeId = $place->getId();
|
||||
|
||||
$places[$placeId] = [
|
||||
'id' => $placeId,
|
||||
'lat' => $place->getLat(),
|
||||
'lng' => $place->getLng(),
|
||||
'panoId' => null,
|
||||
'pov' => $place->getPov()->toArray(),
|
||||
'noPano' => $noPano
|
||||
];
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,24 @@
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Database\RawExpression;
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
|
||||
class MapsController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getMaps(): IContent
|
||||
{
|
||||
//TODO: from repository - count should be added
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns([
|
||||
['maps', 'id'],
|
||||
@@ -20,43 +29,54 @@ class MapsController
|
||||
['maps', 'bound_west_lng'],
|
||||
['maps', 'bound_north_lat'],
|
||||
['maps', 'bound_east_lng'],
|
||||
['maps', 'area'],
|
||||
new RawExpression('COUNT(places.id) AS num_places')
|
||||
]);
|
||||
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
|
||||
$select->groupBy(['maps', 'id']);
|
||||
$select->orderBy('name');
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
$maps = [];
|
||||
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
|
||||
$map['area'] = $this->formatMapAreaForHuman($bounds->calculateApproximateArea());
|
||||
$map['area'] = $this->formatMapAreaForHuman($map['area']);
|
||||
|
||||
$maps[] = $map;
|
||||
}
|
||||
|
||||
$data = ['maps' => $maps];
|
||||
return new HtmlContent('maps', $data);
|
||||
$user = $this->request->user();
|
||||
return new HtmlContent('maps', [
|
||||
'maps' => $maps,
|
||||
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
|
||||
]);
|
||||
}
|
||||
|
||||
private function formatMapAreaForHuman(float $area): array
|
||||
{
|
||||
if ($area < 100000.0) {
|
||||
if ($area < 0.01) {
|
||||
$digits = 0;
|
||||
$rounded = round($area * 1000000.0, -2);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 0.1) {
|
||||
$digits = 0;
|
||||
$rounded = round($area * 1000000.0, -3);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 1.0) {
|
||||
$digits = 2;
|
||||
$rounded = round($area, 2);
|
||||
$unit = 'km';
|
||||
} elseif ($area < 100.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area, 0);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 100000000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, 0);
|
||||
$unit = 'km';
|
||||
} elseif ($area < 10000000000.0) {
|
||||
} elseif ($area < 10000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, -2);
|
||||
$rounded = round($area, -2);
|
||||
$unit = 'km';
|
||||
} else {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, -4);
|
||||
$rounded = round($area, -4);
|
||||
$unit = 'km';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
|
||||
class PositionController
|
||||
{
|
||||
const NUMBER_OF_ROUNDS = 5;
|
||||
const MAX_SCORE = 1000;
|
||||
|
||||
public function getPosition(array $parameters): IContent
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
|
||||
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'No valid session found!'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
if (count($_SESSION['state']['rounds']) === 0) {
|
||||
$newPosition = $this->getNewPosition($mapId);
|
||||
$_SESSION['state']['rounds'][] = $newPosition;
|
||||
|
||||
$data = ['panoId' => $newPosition['panoId']];
|
||||
} else {
|
||||
$rounds = count($_SESSION['state']['rounds']);
|
||||
$last = $_SESSION['state']['rounds'][$rounds - 1];
|
||||
|
||||
$history = [];
|
||||
for ($i = 0; $i < $rounds - 1; ++$i) {
|
||||
$round = $_SESSION['state']['rounds'][$i];
|
||||
$history[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'guessPosition' => $round['guessPosition']->toArray(),
|
||||
'distance' => $round['distance'],
|
||||
'score' => $round['score']
|
||||
];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'history' => $history,
|
||||
'panoId' => $last['panoId']
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
public function evaluateGuess(array $parameters): IContent
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
|
||||
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'No valid session found!'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
$last = &$_SESSION['state']['rounds'][count($_SESSION['state']['rounds']) - 1];
|
||||
|
||||
$position = $last['position'];
|
||||
$guessPosition = new Position((float) $_POST['lat'], (float) $_POST['lng']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
|
||||
$distance = $this->calculateDistance($position, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $_SESSION['state']['area']);
|
||||
|
||||
$last['distance'] = $distance;
|
||||
$last['score'] = $score;
|
||||
|
||||
if (count($_SESSION['state']['rounds']) < static::NUMBER_OF_ROUNDS) {
|
||||
$exclude = [];
|
||||
|
||||
foreach ($_SESSION['state']['rounds'] as $round) {
|
||||
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
|
||||
}
|
||||
|
||||
$newPosition = $this->getNewPosition($mapId, $exclude);
|
||||
$_SESSION['state']['rounds'][] = $newPosition;
|
||||
|
||||
$panoId = $newPosition['panoId'];
|
||||
} else {
|
||||
$_SESSION['state']['rounds'] = [];
|
||||
|
||||
$panoId = null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'result' => [
|
||||
'position' => $position->toArray(),
|
||||
'distance' => $distance,
|
||||
'score' => $score
|
||||
],
|
||||
'panoId' => $panoId
|
||||
];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
private function getNewPosition(int $mapId, array $exclude = []): array
|
||||
{
|
||||
$placesWithoutPano = [];
|
||||
|
||||
do {
|
||||
$place = $this->selectNewPlace($mapId, $exclude);
|
||||
$position = new Position($place['lat'], $place['lng']);
|
||||
$panoId = $this->getPanorama($position);
|
||||
|
||||
if ($panoId === null) {
|
||||
$placesWithoutPano[] = $place['id'];
|
||||
}
|
||||
} while ($panoId === null);
|
||||
|
||||
return [
|
||||
'placesWithoutPano' => $placesWithoutPano,
|
||||
'placeId' => $place['id'],
|
||||
'position' => $position,
|
||||
'panoId' => $panoId
|
||||
];
|
||||
}
|
||||
|
||||
private function selectNewPlace(int $mapId, array $exclude): array
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng']);
|
||||
$select->where('id', 'NOT IN', $exclude);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
|
||||
$numberOfPlaces = $select->count();// TODO: what if 0
|
||||
$randomOffset = random_int(0, $numberOfPlaces - 1);
|
||||
|
||||
$select->orderBy('id');
|
||||
$select->limit(1, $randomOffset);
|
||||
|
||||
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
return $place;
|
||||
}
|
||||
|
||||
private function getPanorama(Position $position): ?string
|
||||
{
|
||||
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
|
||||
$request->setQuery([
|
||||
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
|
||||
'location' => $position->getLat() . ',' . $position->getLng(),
|
||||
'source' => 'outdoor'
|
||||
]);
|
||||
|
||||
$response = $request->send();
|
||||
|
||||
$panoData = json_decode($response->getBody(), true);
|
||||
|
||||
if ($panoData['status'] !== 'OK') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $panoData['pano_id'];
|
||||
}
|
||||
|
||||
private function calculateDistance(Position $realPosition, Position $guessPosition): float
|
||||
{
|
||||
return $realPosition->calculateDistanceTo($guessPosition);
|
||||
}
|
||||
|
||||
private function calculateScore(float $distance, float $area): int
|
||||
{
|
||||
$goodness = 1.0 - ($distance / sqrt($area));
|
||||
|
||||
return (int) round(pow(static::MAX_SCORE, $goodness));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Response\Redirect;
|
||||
use MapGuesser\Util\JwtParser;
|
||||
|
||||
class UserController implements ISecured
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserConfirmationRepository $userConfirmationRepository;
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->request->user();
|
||||
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
public function getAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
return new HtmlContent('account/account', ['user' => $user->toArray()]);
|
||||
}
|
||||
|
||||
public function getGoogleAuthenticateRedirect(): IRedirect
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
$state = bin2hex(random_bytes(16));
|
||||
$nonce = bin2hex(random_bytes(16));
|
||||
|
||||
$this->request->session()->set('oauth_state', $state);
|
||||
$this->request->session()->set('oauth_nonce', $nonce);
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
|
||||
$url = $oAuth->getDialogUrl(
|
||||
$state,
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink(),
|
||||
$nonce,
|
||||
$user->getEmail()
|
||||
);
|
||||
|
||||
return new Redirect($url, IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
public function authenticateWithGoogle(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
$oAuth = new GoogleOAuth(new Request());
|
||||
$tokenData = $oAuth->getToken(
|
||||
$this->request->query('code'),
|
||||
$this->request->getBase() . '/' . \Container::$routeCollection->getRoute('account.googleAuthenticate-action')->generateLink()
|
||||
);
|
||||
|
||||
if (!isset($tokenData['id_token'])) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
$jwtParser = new JwtParser($tokenData['id_token']);
|
||||
$idToken = $jwtParser->getPayload();
|
||||
|
||||
if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) {
|
||||
return new HtmlContent('account/google_authenticate', ['success' => false]);
|
||||
}
|
||||
|
||||
if ($idToken['sub'] !== $user->getGoogleSub()) {
|
||||
return new HtmlContent('account/google_authenticate', [
|
||||
'success' => false,
|
||||
'errorText' => 'This Google account is not linked to your account.'
|
||||
]);
|
||||
}
|
||||
|
||||
$authenticatedWithGoogleUntil = new DateTime('+45 seconds');
|
||||
$this->request->session()->set('authenticated_with_google_until', $authenticatedWithGoogleUntil);
|
||||
|
||||
return new HtmlContent('account/google_authenticate', [
|
||||
'success' => true,
|
||||
'authenticatedWithGoogleUntil' => $authenticatedWithGoogleUntil
|
||||
]);
|
||||
}
|
||||
|
||||
public function getDeleteAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
return new HtmlContent('account/delete', ['user' => $user->toArray()]);
|
||||
}
|
||||
|
||||
public function saveAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if (!$this->confirmUserIdentity(
|
||||
$user,
|
||||
$this->request->session()->get('authenticated_with_google_until'),
|
||||
$this->request->post('password'),
|
||||
$error
|
||||
)) {
|
||||
return new JsonContent(['error' => ['errorText' => $error]]);
|
||||
}
|
||||
|
||||
if (strlen($this->request->post('password_new')) > 0) {
|
||||
if (strlen($this->request->post('password_new')) < 6) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given new password is too short. Please choose a password that is at least 6 characters long!'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->post('password_new') !== $this->request->post('password_new_confirm')) {
|
||||
return new JsonContent([
|
||||
'error' => [
|
||||
'errorText' => 'The given new passwords do not match.'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$user->setPlainPassword($this->request->post('password_new'));
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($user);
|
||||
|
||||
$this->request->session()->delete('authenticated_with_google_until');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
public function deleteAccount(): IContent
|
||||
{
|
||||
/**
|
||||
* @var User $user
|
||||
*/
|
||||
$user = $this->request->user();
|
||||
|
||||
if (!$this->confirmUserIdentity(
|
||||
$user,
|
||||
$this->request->session()->get('authenticated_with_google_until'),
|
||||
$this->request->post('password'),
|
||||
$error
|
||||
)) {
|
||||
return new JsonContent(['error' => ['errorText' => $error]]);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->startTransaction();
|
||||
|
||||
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
|
||||
if ($userConfirmation !== null) {
|
||||
$this->pdm->deleteFromDb($userConfirmation);
|
||||
}
|
||||
|
||||
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
|
||||
if ($userPasswordResetter !== null) {
|
||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||
}
|
||||
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
|
||||
$this->request->session()->delete('authenticated_with_google_until');
|
||||
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool
|
||||
{
|
||||
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
if ($user->checkPassword($password)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$error = 'The given current password is wrong.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$error = 'Could not confirm your identity. Please try again!';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,11 @@ class Connection implements IConnection
|
||||
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
|
||||
{
|
||||
if ($port < 0) {
|
||||
$port = ini_get('mysqli.default_port');
|
||||
$port = (int) ini_get('mysqli.default_port');
|
||||
}
|
||||
|
||||
if ($socket === null) {
|
||||
$socket = ini_get('mysqli.default_socket');
|
||||
$socket = (string) ini_get('mysqli.default_socket');
|
||||
}
|
||||
|
||||
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
|
||||
|
||||