Compare commits
245
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d30ec3a3a0 | ||
|
|
cc19d454fa
|
||
|
|
241d2f2b30
|
||
|
|
d0751017db
|
||
|
|
6014e4517a | ||
|
|
81a8153866
|
||
|
|
e435ef6be0 | ||
|
|
90528e61e5
|
||
|
|
d02aa4abe0
|
||
|
|
4f45e213c3 | ||
|
|
d877d2fe03
|
||
|
|
66240c2d65 | ||
|
|
0654b65940
|
||
|
|
272ec3568d | ||
|
|
be4a2038e5
|
||
|
|
f305c97513
|
||
|
|
784037de6f | ||
|
|
6f27450423 | ||
|
|
bbaa2fe1eb | ||
|
|
28165d76d3 | ||
|
|
1c1e5f051d | ||
|
|
283c214c50 | ||
|
|
7970927654 | ||
|
|
567602c749 | ||
|
|
a5238234d2 | ||
|
|
c965713c9c | ||
|
|
b00fe4ebe3 | ||
|
|
599d8bf153 | ||
|
|
636c47366a | ||
|
|
4520d11559 | ||
|
|
5f0b639fe7 | ||
|
|
d4a279d2f4 | ||
|
|
aadeda05af | ||
|
|
b78d564f6f | ||
|
|
c2df9b7713 | ||
|
|
bbb66ca979 | ||
|
|
3b98570f6d | ||
|
|
77ff175794 | ||
|
|
ae7fd98256 | ||
|
|
517d758e8e | ||
|
|
2a1b51b014 | ||
|
|
c085a5a1ca | ||
|
|
a7cfc3c19f | ||
|
|
7898c52328 | ||
|
|
62251b1062 | ||
|
|
069c6b37c8 | ||
|
|
30f4b7ad19 | ||
|
|
45ddb7f56a | ||
|
|
7792f1c3ff | ||
|
|
7a1674fdd0 | ||
|
|
44df94eb98 | ||
|
|
00afd65d75 | ||
|
|
21e41b7c36 | ||
|
|
69964acfb2 | ||
|
|
e7869d67f7 | ||
|
|
93f8fc3f34 | ||
|
|
5daed10036 | ||
|
|
d7147b30d6 | ||
|
|
c02f595606 | ||
|
|
99244e93d2 | ||
|
|
313a3568aa | ||
|
|
54bc9c31db
|
||
|
|
75bea7c05c | ||
|
|
1c3238e64e | ||
|
|
6cb9e17ddf | ||
|
|
88ad6e9d61
|
||
|
|
2fb807c479
|
||
|
|
8e9e5b08f9
|
||
|
|
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
|
No files matched your search
@@ -7,6 +7,7 @@ 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
|
||||
@@ -15,3 +16,10 @@ 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
|
||||
ENABLE_GAME_FOR_GUESTS=0
|
||||
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
|
||||
RECAPTCHA_SECRET=your_recaptcha_secret
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.env
|
||||
installed
|
||||
unit_test_results.xml
|
||||
vendor
|
||||
node_modules
|
||||
Vendored
+8
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'mapguesser'
|
||||
customWorkspace 'workspace/mapguesser'
|
||||
}
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Install composer') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
dir '.'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'composer install'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Unit Testing') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
dir '.'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
archiveArtifacts 'unit_test_results.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Static Code Analysis') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
dir '.'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
archiveArtifacts 'static_code_analysis_results.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
# MapGuesser
|
||||
|
||||
[](https://jenkins.e5tv.hu/job/mapguesser/job/develop/)
|
||||
|
||||
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.
|
||||
|
||||
## Installation
|
||||
@@ -9,12 +11,12 @@ This is the MapGuesser Application project. This is a game about guessing where
|
||||
The first step is obviously cloning the repository to your machine:
|
||||
|
||||
```
|
||||
git clone https://bitbucket.org/esoko/mapguesser.git
|
||||
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
|
||||
```
|
||||
|
||||
All the commands listed here should be executed from the repository root.
|
||||
|
||||
### (Optional) Setup Docker stack
|
||||
### 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`.
|
||||
|
||||
@@ -24,9 +26,17 @@ All you have to do is executing the following command:
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Attach shell to the container of `mapguesser_app`. All of the following commands should be executed there.
|
||||
Attach shell to the container of `mapguesser_app`:
|
||||
|
||||
**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.**
|
||||
```
|
||||
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
|
||||
|
||||
@@ -40,15 +50,34 @@ composer create-project
|
||||
|
||||
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
|
||||
|
||||
**You should set here 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/**
|
||||
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
|
||||
|
||||
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in produciton 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.
|
||||
|
||||
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, 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
|
||||
LEAFLET_TILESERVER_ATTRIBUTION="© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
|
||||
```
|
||||
|
||||
### (Production only) Create cron job
|
||||
|
||||
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommened to create a cron job that runs every hour:
|
||||
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
|
||||
@@ -56,13 +85,19 @@ To maintain database (delete inactive users, old sessions etc.), the command `db
|
||||
|
||||
### Finalize installation
|
||||
|
||||
After you set the environment variables in the `.env` file, execute the following command:
|
||||
After you followed the above steps, execute the following command:
|
||||
|
||||
```
|
||||
scripts/install.sh
|
||||
```
|
||||
|
||||
**And you are done!** The application is ready to use and develop.
|
||||
**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.
|
||||
|
||||
|
||||
@@ -13,3 +13,14 @@ pipelines:
|
||||
- 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
|
||||
+4
-2
@@ -6,10 +6,12 @@
|
||||
"require": {
|
||||
"vlucas/phpdotenv": "^4.1",
|
||||
"symfony/console": "^5.1",
|
||||
"phpmailer/phpmailer": "^6.1"
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"fzaninotto/faker": "^1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9"
|
||||
"phpunit/phpunit": "^9",
|
||||
"phpstan/phpstan": "^0.12.32"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
Generated
+113
-2
@@ -4,8 +4,63 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fb37b0b958cdf02d82468cb4596613ad",
|
||||
"content-hash": "b71c0ffc0761a6b90f6242346b735a09",
|
||||
"packages": [
|
||||
{
|
||||
"name": "fzaninotto/faker",
|
||||
"version": "v1.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fzaninotto/Faker.git",
|
||||
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/fzaninotto/Faker/zipball/848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
|
||||
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3.3 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-intl": "*",
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7",
|
||||
"squizlabs/php_codesniffer": "^2.9.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Faker\\": "src/Faker/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "François Zaninotto"
|
||||
}
|
||||
],
|
||||
"description": "Faker is a PHP library that generates fake data for you.",
|
||||
"keywords": [
|
||||
"data",
|
||||
"faker",
|
||||
"fixtures"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/fzaninotto/Faker/issues",
|
||||
"source": "https://github.com/fzaninotto/Faker/tree/v1.9.2"
|
||||
},
|
||||
"abandoned": true,
|
||||
"time": "2020-12-11T09:56:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v6.1.6",
|
||||
@@ -1379,6 +1434,62 @@
|
||||
],
|
||||
"time": "2020-03-05T15:02:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "0.12.32",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "d03863f504c8432b3de4d1881f73f6acb8c0e67c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03863f504c8432b3de4d1881f73f6acb8c0e67c",
|
||||
"reference": "d03863f504c8432b3de4d1881f73f6acb8c0e67c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.12-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpstan",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-01T11:57:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "8.0.2",
|
||||
@@ -2654,5 +2765,5 @@
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "1.1.0"
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,54 @@
|
||||
CREATE TABLE `challenges` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`token` int(10) unsigned NOT NULL,
|
||||
`time_limit` int(10) unsigned,
|
||||
`time_limit_type` enum('game', 'round') NOT NULL DEFAULT 'game',
|
||||
`no_move` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`no_pan` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`no_zoom` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
CREATE TABLE `user_in_challenge` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`challenge_id` int(10) unsigned NOT NULL,
|
||||
`current_round` smallint(5) signed NOT NULL DEFAULT 0,
|
||||
`time_left` int(10) unsigned,
|
||||
`is_owner` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `challenge_id` (`challenge_id`),
|
||||
CONSTRAINT `user_in_challenge_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `user_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
CREATE TABLE `place_in_challenge` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`place_id` int(10) unsigned NOT NULL,
|
||||
`challenge_id` int(10) unsigned NOT NULL,
|
||||
`round` smallint(5) unsigned NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `place_id` (`place_id`),
|
||||
KEY `challenge_id` (`challenge_id`),
|
||||
CONSTRAINT `place_in_challenge_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`),
|
||||
CONSTRAINT `place_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`),
|
||||
CONSTRAINT `unique_order_in_challenge` UNIQUE (`round`, `challenge_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
CREATE TABLE `guesses` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`place_in_challenge_id` int(10) unsigned NOT NULL,
|
||||
`lat` decimal(8,6) NOT NULL,
|
||||
`lng` decimal(9,6) NOT NULL,
|
||||
`score` int(10) NOT NULL,
|
||||
`distance` int(10) NOT NULL,
|
||||
`time_spent` int(10),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `place_in_challenge_id` (`place_in_challenge_id`),
|
||||
CONSTRAINT `guesses_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `guesses_place_in_challenge_id` FOREIGN KEY (`place_in_challenge_id`) REFERENCES `place_in_challenge` (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `maps`
|
||||
ADD `unlisted` TINYINT(1) NOT NULL DEFAULT 0;
|
||||
+13
-4
@@ -1,14 +1,23 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build: ./docker
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-app
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
links:
|
||||
- 'mariadb'
|
||||
- 'mail'
|
||||
multi:
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-multi
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 8090:8090
|
||||
- 9229:9229
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
mariadb:
|
||||
image: mariadb:10.3
|
||||
ports:
|
||||
|
||||
File renamed without changes.
@@ -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,4 @@
|
||||
FROM php:7.4.7-cli-buster
|
||||
|
||||
RUN apt-get update && apt-get install -y unzip
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
+369
@@ -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);
|
||||
Generated
+17
@@ -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
|
||||
+118
-3
@@ -15,6 +15,17 @@
|
||||
right: 0;
|
||||
background-color: #000000;
|
||||
opacity: 0.5;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
#panningBlockerCover {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@@ -22,7 +33,7 @@
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#guess.result {
|
||||
@@ -79,7 +90,7 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
|
||||
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -102,13 +113,105 @@
|
||||
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;
|
||||
}
|
||||
|
||||
#goToStart {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#highscoresTable {
|
||||
margin: 1em;
|
||||
border-collapse: collapse;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#highscoresTable td, #highscoresTable th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#highscoresTable tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
#highscoresTable tr:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
#highscoresTable th {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
text-align: left;
|
||||
background-color: #e8a349;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#highscoresTable tr.ownPlayer {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 899px) {
|
||||
.hideOnNarrowScreen {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
#mapName {
|
||||
display: none;
|
||||
}
|
||||
#showGuessButtonContainer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
left: 65px;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 2;
|
||||
@@ -125,6 +228,10 @@
|
||||
#scoreBarBase {
|
||||
width: 100%;
|
||||
}
|
||||
#navigation {
|
||||
bottom: 25px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
@@ -168,6 +275,10 @@
|
||||
#scoreBarBase {
|
||||
width: 60%;
|
||||
}
|
||||
#navigation {
|
||||
bottom: 50px;
|
||||
left: 20px;
|
||||
}
|
||||
@media screen and (max-height: 424px) {
|
||||
#guess {
|
||||
top: 10px;
|
||||
@@ -182,5 +293,9 @@
|
||||
right: 20px;
|
||||
bottom: 30px;
|
||||
}
|
||||
#navigation {
|
||||
bottom: 30px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#map {
|
||||
.map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -7,6 +7,13 @@
|
||||
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;
|
||||
@@ -48,6 +55,12 @@
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#overlayControl {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#deleteButton {
|
||||
display: none;
|
||||
}
|
||||
@@ -65,6 +78,9 @@
|
||||
#placeControl {
|
||||
top: calc(50% + 10px);
|
||||
}
|
||||
.hideOnMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px), (max-height: 599px) {
|
||||
|
||||
@@ -12,6 +12,10 @@ html, body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner, input::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
@@ -27,11 +31,11 @@ main {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
p, h1, h2, input, textarea, select, button, a {
|
||||
p, h1, h2, h3, input, textarea, select, button, a, table, label {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
h1, h2, h3 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -51,7 +55,11 @@ h2, header.small h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p, h2 {
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p, h2, h3 {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
@@ -233,7 +241,7 @@ button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.
|
||||
background-color: #1b7d31;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
input.text, select, textarea {
|
||||
background-color: #f9fafb;
|
||||
border: solid #c8d2e1 1px;
|
||||
border-radius: 2px;
|
||||
@@ -242,22 +250,26 @@ input, select, textarea {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
input, select {
|
||||
input.text, select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
input[type=checkbox], input[type=radio] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: 5px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input.big, select.big, textarea.big, div.inputWithButton>input {
|
||||
input.text.big, select.big, textarea.big, div.inputWithButton>input.text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input.big, select.big, div.inputWithButton>input {
|
||||
input.text.big, select.big, div.inputWithButton>input.text {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 6px;
|
||||
@@ -272,19 +284,19 @@ input.fullWidth, select.fullWidth, textarea.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:disabled, select:disabled, textarea:disabled {
|
||||
input.text:disabled, select:disabled, textarea:disabled {
|
||||
background-color: #dfdfdf;
|
||||
border: solid #dfdfdf 1px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
input.text:focus, select:focus, textarea:focus {
|
||||
background-color: #ffffff;
|
||||
border: solid #29457f 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
input.text:focus, select:focus {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
@@ -292,16 +304,16 @@ textarea:focus {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
input.big:focus, select.big:focus {
|
||||
input.text.big:focus, select.big:focus {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input {
|
||||
div.inputWithButton>input.text {
|
||||
width: 100%;
|
||||
padding: 0 83px 0 6px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input:focus {
|
||||
div.inputWithButton>input.text:focus {
|
||||
padding: 0 82px 0 5px;
|
||||
}
|
||||
|
||||
@@ -374,6 +386,7 @@ header>p>span:not(:last-child) {
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: #ffffff;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
@@ -404,9 +417,17 @@ div.buttonContainer>button {
|
||||
}
|
||||
|
||||
#cookiesNotice {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 20px;
|
||||
background-color: #eeeeee;
|
||||
border: solid #888888 1px;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#loading {
|
||||
@@ -430,6 +451,45 @@ div.box {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.circleControl {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.circleControl .controlItem {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
margin-top: 10px;
|
||||
opacity: 70%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.circleControl .controlItem:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.circleControl .controlItem div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.circleControl .controlBackground {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.circleControl .controlIcon {
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
margin: auto;
|
||||
margin-top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
header h1 span {
|
||||
display: none;
|
||||
@@ -456,6 +516,12 @@ div.box {
|
||||
div.box {
|
||||
width: initial;
|
||||
}
|
||||
.circleControl {
|
||||
width: 45px;
|
||||
}
|
||||
.circleControl .controlItem {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
@@ -485,6 +551,12 @@ div.box {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.circleControl {
|
||||
width: 45px;
|
||||
}
|
||||
.circleControl .controlItem {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 400px) and (max-height: 499px) {
|
||||
|
||||
@@ -13,6 +13,10 @@ div.mapItem.new {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.mapItem.unlisted {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
@@ -75,6 +79,10 @@ div.mapItem>div.buttonContainer {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
#timeLimitType {
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto auto auto;
|
||||
|
||||
@@ -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,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 |
@@ -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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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,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 |
+879
-102
File diff suppressed because it is too large.
Load diff
+375
-75
@@ -7,7 +7,6 @@
|
||||
description: null
|
||||
},
|
||||
map: null,
|
||||
markers: null,
|
||||
panorama: null,
|
||||
selectedMarker: null,
|
||||
added: {},
|
||||
@@ -19,6 +18,7 @@
|
||||
|
||||
MapEditor.metadata.name = form.elements.name.value;
|
||||
MapEditor.metadata.description = form.elements.description.value;
|
||||
MapEditor.metadata.unlisted = form.elements.unlisted.checked;
|
||||
|
||||
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
|
||||
|
||||
@@ -87,14 +87,14 @@
|
||||
sv.getPanorama({
|
||||
location: location,
|
||||
preference: google.maps.StreetViewPreference.NEAREST,
|
||||
radius: 100,
|
||||
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;
|
||||
if (panoLocationData === null && !canBeIndoor) {
|
||||
MapEditor.requestPanoData(location, true);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
@@ -118,16 +118,13 @@
|
||||
MapEditor.resetSelected();
|
||||
MapEditor.selectedMarker = marker;
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
MapEditor.map.resize();
|
||||
MapEditor.map.panTo(marker.getLatLng());
|
||||
|
||||
MapEditor.panorama.setVisible(false);
|
||||
|
||||
if (marker.placeId) {
|
||||
MapEditor.markers.removeLayer(MapEditor.selectedMarker);
|
||||
MapEditor.map.addLayer(MapEditor.selectedMarker);
|
||||
marker.setIcon(IconCollection.iconBlue);
|
||||
marker.setZIndexOffset(2000);
|
||||
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'block';
|
||||
|
||||
@@ -165,13 +162,13 @@
|
||||
var placeId = MapEditor.selectedMarker.placeId
|
||||
|
||||
if (places[placeId].id && !del) {
|
||||
MapEditor.map.removeLayer(MapEditor.selectedMarker);
|
||||
MapEditor.markers.addLayer(MapEditor.selectedMarker);
|
||||
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
|
||||
MapEditor.selectedMarker.setZIndexOffset(1000);
|
||||
MapEditor.map.changeMarkerIcon(
|
||||
MapEditor.selectedMarker,
|
||||
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
|
||||
);
|
||||
} else {
|
||||
delete places[placeId];
|
||||
MapEditor.map.removeLayer(MapEditor.selectedMarker);
|
||||
MapEditor.map.removeMarker(MapEditor.selectedMarker);
|
||||
}
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'none';
|
||||
@@ -223,7 +220,7 @@
|
||||
MapEditor.resetSelected(del);
|
||||
MapEditor.selectedMarker = null;
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
MapEditor.map.resize();
|
||||
},
|
||||
|
||||
deletePlace: function () {
|
||||
@@ -239,6 +236,7 @@
|
||||
|
||||
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);
|
||||
@@ -257,6 +255,9 @@
|
||||
if (MapEditor.metadata.description !== null) {
|
||||
data.append('description', MapEditor.metadata.description);
|
||||
}
|
||||
if (MapEditor.metadata.unlisted !== null) {
|
||||
data.append('unlisted', MapEditor.metadata.unlisted);
|
||||
}
|
||||
|
||||
for (var placeId in MapEditor.added) {
|
||||
if (!MapEditor.added.hasOwnProperty(placeId)) {
|
||||
@@ -313,24 +314,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
var 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]
|
||||
}),
|
||||
};
|
||||
|
||||
var Util = {
|
||||
getHighResData: function () {
|
||||
if (window.devicePixelRatio >= 4) {
|
||||
@@ -340,64 +323,337 @@
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
|
||||
MapEditor.map = L.map('map', {
|
||||
zoomControl: false
|
||||
});
|
||||
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]
|
||||
})
|
||||
},
|
||||
|
||||
MapEditor.map.on('click', function (e) {
|
||||
var marker = L.marker(e.latlng, {
|
||||
icon: IconCollection.iconBlue,
|
||||
zIndexOffset: 2000
|
||||
})
|
||||
.addTo(MapEditor.map)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
init: function (divId, places) {
|
||||
document.getElementById(divId).style.display = "block";
|
||||
|
||||
MapEditor.select(marker);
|
||||
});
|
||||
if (!LMapWrapper.map) {
|
||||
LMapWrapper.divId = divId;
|
||||
LMapWrapper.map = L.map(LMapWrapper.divId, {
|
||||
center: { lat: 0., lng: 0. },
|
||||
zoom: 2
|
||||
});
|
||||
|
||||
var highResData = Util.getHighResData();
|
||||
LMapWrapper.map.on('click', function (e) {
|
||||
LMapWrapper.placeMarker(e.latlng);
|
||||
});
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
attribution: tileAttribution,
|
||||
subdomains: '1234',
|
||||
ppi: highResData.ppi,
|
||||
tileSize: highResData.tileSize,
|
||||
zoomOffset: highResData.zoomOffset,
|
||||
minZoom: highResData.minZoom,
|
||||
maxZoom: highResData.maxZoom
|
||||
}).addTo(MapEditor.map);
|
||||
var highResData = Util.getHighResData();
|
||||
|
||||
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||
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);
|
||||
|
||||
MapEditor.markers = L.markerClusterGroup({
|
||||
maxClusterRadius: 50
|
||||
});
|
||||
if (mapId) {
|
||||
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||
}
|
||||
}
|
||||
|
||||
for (var placeId in places) {
|
||||
if (!places.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
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 place = places[placeId];
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
|
||||
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
|
||||
zIndexOffset: 1000
|
||||
})
|
||||
.addTo(MapEditor.markers)
|
||||
.on('click', function () {
|
||||
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);
|
||||
});
|
||||
|
||||
marker.placeId = place.id;
|
||||
}
|
||||
GMapWrapper.markers.addMarker(marker);
|
||||
|
||||
MapEditor.map.addLayer(MapEditor.markers);
|
||||
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
|
||||
@@ -440,4 +696,48 @@
|
||||
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();
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,4 +1,5 @@
|
||||
var MapGuesser = {
|
||||
isSecure: window.location.protocol === 'https:',
|
||||
cookiesAgreed: false,
|
||||
sessionAvailableHooks: {},
|
||||
|
||||
|
||||
@@ -65,6 +65,32 @@
|
||||
}
|
||||
};
|
||||
|
||||
var Util = {
|
||||
printTimeForHuman: function (time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = time % 60;
|
||||
var time_str = '';
|
||||
|
||||
if (minutes == 1) {
|
||||
time_str += '1 minute';
|
||||
} else if (minutes > 1) {
|
||||
time_str += minutes + ' minutes';
|
||||
}
|
||||
|
||||
if (minutes > 0 && seconds > 0) {
|
||||
time_str += ' and ';
|
||||
}
|
||||
|
||||
if (seconds == 1) {
|
||||
time_str += '1 second';
|
||||
} else if (seconds > 1) {
|
||||
time_str += seconds + ' seconds';
|
||||
}
|
||||
|
||||
return time_str;
|
||||
}
|
||||
};
|
||||
|
||||
Maps.addStaticMaps();
|
||||
|
||||
Maps.initializeDescriptionDivs();
|
||||
@@ -73,4 +99,83 @@
|
||||
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('challengeForm').onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var url = '/challenge/create.json';
|
||||
var formData = new FormData(this);
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
MapGuesser.httpRequest('POST', url, function() {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = '/challenge/' + this.response.challengeToken;
|
||||
|
||||
}, formData);
|
||||
};
|
||||
|
||||
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('playMode').style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
if (document.getElementById('challengeButton')) {
|
||||
document.getElementById('challengeButton').onclick = function () {
|
||||
MapGuesser.showModal('challenge');
|
||||
document.getElementById('createNewChallengeButton').href = '/challenge/new/' + this.dataset.mapId;
|
||||
document.getElementById('playMode').style.visibility = 'hidden';
|
||||
|
||||
var timeLimit = document.getElementById('timeLimit').value;
|
||||
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('closePlayModeButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
document.getElementById('closeMultiButton').onclick = function () {
|
||||
MapGuesser.hideModal();
|
||||
};
|
||||
|
||||
document.getElementById('closeChallengeButton').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;
|
||||
document.getElementById('challengeMapId').value = this.dataset.mapId;
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('timeLimit').oninput = function () {
|
||||
var timeLimit = document.getElementById('timeLimit').value;
|
||||
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
|
||||
document.getElementById('timerEnabled').checked = true;
|
||||
}
|
||||
})();
|
||||
@@ -59,10 +59,25 @@ def getRevisionForRef(ref):
|
||||
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
||||
|
||||
def getLatestReleaseTag():
|
||||
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
|
||||
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"], cwd=REPO)
|
||||
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
|
||||
|
||||
def checkoutWorktree(worktreePath, ref):
|
||||
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
||||
|
||||
+7
-1
@@ -9,11 +9,14 @@ if [ -f ${ROOT_DIR}/installed ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
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}/db/mapguesser.sql
|
||||
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)
|
||||
@@ -24,6 +27,9 @@ if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
|
||||
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
|
||||
@@ -11,6 +11,9 @@ 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)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
public function configure()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('user:add')
|
||||
->setDescription('Adding of user.')
|
||||
@@ -27,7 +27,7 @@ class AddUserCommand extends Command
|
||||
$user->setActive(true);
|
||||
$user->setCreatedDate(new DateTime());
|
||||
|
||||
if ($input->hasArgument('type')) {
|
||||
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
|
||||
$user->setType($input->getArgument('type'));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class LinkViewCommand extends Command
|
||||
{
|
||||
public function configure()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('view:link')
|
||||
->setDescription('Linking of views.')
|
||||
@@ -39,7 +39,7 @@ class LinkViewCommand extends Command
|
||||
|
||||
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
|
||||
|
||||
if (strpos($view, 'templates') === 0) {
|
||||
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ 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;
|
||||
@@ -14,7 +16,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MaintainDatabaseCommand extends Command
|
||||
{
|
||||
private PersistentDataManager $persistentDataManager;
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
@@ -22,17 +24,23 @@ class MaintainDatabaseCommand extends Command
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->persistentDataManager = new PersistentDataManager();
|
||||
$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()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('db:maintain')
|
||||
->setDescription('Maintain database.');
|
||||
@@ -43,6 +51,7 @@ class MaintainDatabaseCommand extends Command
|
||||
try {
|
||||
$this->deleteInactiveExpiredUsers();
|
||||
$this->deleteExpiredPasswordResetters();
|
||||
$this->deleteExpiredRooms();
|
||||
$this->deleteExpiredSessions();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Maintenance failed!</error>');
|
||||
@@ -76,7 +85,11 @@ class MaintainDatabaseCommand extends Command
|
||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||
}
|
||||
|
||||
$this->persistentDataManager->deleteFromDb($user);
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
}
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
@@ -85,7 +98,14 @@ class MaintainDatabaseCommand extends Command
|
||||
private function deleteExpiredPasswordResetters(): void
|
||||
{
|
||||
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
|
||||
$this->persistentDataManager->deleteFromDb($passwordResetter);
|
||||
$this->pdm->deleteFromDb($passwordResetter);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteExpiredRooms(): void
|
||||
{
|
||||
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
|
||||
$this->pdm->deleteFromDb($multiRoom);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MigrateDatabaseCommand extends Command
|
||||
{
|
||||
public function configure()
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setName('db:migrate')
|
||||
->setDescription('Migration of database changes.');
|
||||
@@ -88,6 +88,10 @@ class MigrateDatabaseCommand extends Command
|
||||
$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;
|
||||
|
||||
@@ -1,51 +1,302 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use Faker\Factory;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
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\Challenge;
|
||||
use MapGuesser\PersistentData\Model\MultiRoom;
|
||||
use MapGuesser\PersistentData\Model\PlaceInChallenge;
|
||||
use MapGuesser\PersistentData\Model\UserInChallenge;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\ChallengeRepository;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Repository\UserInChallengeRepository;
|
||||
use MapGuesser\Response\Redirect;
|
||||
|
||||
class GameController
|
||||
class GameController implements ISecured
|
||||
{
|
||||
const NUMBER_OF_ROUNDS = 5;
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
private ChallengeRepository $challengeRepository;
|
||||
|
||||
private UserInChallengeRepository $userInChallengeRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->mapRepository = new MapRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
$this->challengeRepository = new ChallengeRepository();
|
||||
$this->userInChallengeRepository = new UserInChallengeRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
|
||||
}
|
||||
|
||||
public function getGame(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
return new HtmlContent('game', $this->prepareGame($mapId));
|
||||
return new HtmlContent('game', ['mapId' => $mapId]);
|
||||
}
|
||||
|
||||
public function getGameJson(): IContent
|
||||
public function getNewMultiGame(): IRedirect
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$roomId = bin2hex(random_bytes(3));
|
||||
$token = $this->getMultiToken($roomId);
|
||||
|
||||
return new JsonContent($this->prepareGame($mapId));
|
||||
$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
|
||||
);
|
||||
}
|
||||
|
||||
private function prepareGame(int $mapId)
|
||||
public function getMultiGame(): IContent
|
||||
{
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$roomId = $this->request->query('roomId');
|
||||
|
||||
return new HtmlContent('game', ['roomId' => $roomId]);
|
||||
}
|
||||
|
||||
public function getChallenge(): IContent
|
||||
{
|
||||
$challengeToken = $this->request->query('challengeToken');
|
||||
|
||||
return new HtmlContent('game', ['challengeToken' => $challengeToken]);
|
||||
}
|
||||
|
||||
public function createNewChallenge(): IContent
|
||||
{
|
||||
// create Challenge
|
||||
do {
|
||||
// initiliaze or if a challenge with the same token already exists
|
||||
$challengeToken = mt_rand();
|
||||
} while ($this->challengeRepository->getByToken($challengeToken));
|
||||
|
||||
$challenge = new Challenge();
|
||||
$challenge->setToken($challengeToken);
|
||||
$challenge->setCreatedDate(new DateTime());
|
||||
|
||||
if ($this->request->post('timerEnabled') !== null && $this->request->post('timeLimit') !== null) {
|
||||
$challenge->setTimeLimit($this->request->post('timeLimit'));
|
||||
}
|
||||
if ($this->request->post('timeLimitType') !== null) {
|
||||
$challenge->setTimeLimitType($this->request->post('timeLimitType'));
|
||||
}
|
||||
if ($this->request->post('noMove') !== null) {
|
||||
$challenge->setNoMove(true);
|
||||
}
|
||||
if ($this->request->post('noPan') !== null) {
|
||||
$challenge->setNoPan(true);
|
||||
}
|
||||
if ($this->request->post('noZoom') !== null) {
|
||||
$challenge->setNoZoom(true);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($challenge);
|
||||
|
||||
// save owner/creator
|
||||
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
|
||||
$userInChallenge = new UserInChallenge();
|
||||
$userInChallenge->setUserId($userId);
|
||||
$userInChallenge->setChallenge($challenge);
|
||||
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
|
||||
$userInChallenge->setIsOwner(true);
|
||||
|
||||
$this->pdm->saveToDb($userInChallenge);
|
||||
|
||||
// select places
|
||||
|
||||
$mapId = (int) $this->request->post('mapId');
|
||||
// $map = $this->mapRepository->getById($mapId);
|
||||
|
||||
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
|
||||
|
||||
$round = 0;
|
||||
foreach ($places as $place) {
|
||||
$placeInChallenge = new PlaceInChallenge();
|
||||
$placeInChallenge->setPlace($place);
|
||||
$placeInChallenge->setChallenge($challenge);
|
||||
$placeInChallenge->setRound($round++);
|
||||
$this->pdm->saveToDb($placeInChallenge);
|
||||
}
|
||||
|
||||
return new JsonContent(['challengeToken' => dechex($challengeToken)]);
|
||||
}
|
||||
|
||||
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' => $map->getArea(),
|
||||
'rounds' => []
|
||||
'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, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
|
||||
return new JsonContent([
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'bounds' => $map->getBounds()->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
public function prepareMultiGame(): IContent
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
$userName = $this->request->post('userName');
|
||||
if (empty($userName)) {
|
||||
$faker = Factory::create();
|
||||
$userName = $faker->userName;
|
||||
}
|
||||
|
||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||
|
||||
if (!isset($room)) {
|
||||
return new JsonContent(['error' => 'game_not_found']);
|
||||
}
|
||||
|
||||
$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()
|
||||
]);
|
||||
}
|
||||
|
||||
public function prepareChallenge(): IContent
|
||||
{
|
||||
$challengeToken_str = $this->request->query('challengeToken');
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
|
||||
if (!isset($userId))
|
||||
{
|
||||
return new JsonContent(['error' => 'anonymous_user']);
|
||||
}
|
||||
|
||||
$challenge = $this->challengeRepository->getByTokenStr($challengeToken_str);
|
||||
|
||||
if (!isset($challenge))
|
||||
{
|
||||
return new JsonContent(['error' => 'game_not_found']);
|
||||
}
|
||||
|
||||
if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) {
|
||||
// new player is joining
|
||||
$userInChallenge = new UserInChallenge();
|
||||
$userInChallenge->setUserId($userId);
|
||||
$userInChallenge->setChallenge($challenge);
|
||||
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
|
||||
$this->pdm->saveToDb($userInChallenge);
|
||||
}
|
||||
|
||||
$map = $this->mapRepository->getByChallenge($challenge);
|
||||
|
||||
return new JsonContent([
|
||||
'mapId' => $map->getId(),
|
||||
'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;
|
||||
}
|
||||
}
|
||||
@@ -1,142 +1,476 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
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\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Guess;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\PlaceInChallenge;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserPlayedPlace;
|
||||
use MapGuesser\Repository\ChallengeRepository;
|
||||
use MapGuesser\Repository\GuessRepository;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\PlaceInChallengeRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Repository\UserInChallengeRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
|
||||
class GameFlowController
|
||||
class GameFlowController implements ISecured
|
||||
{
|
||||
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 MapRepository $mapRepository;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
private ChallengeRepository $challengeRepository;
|
||||
|
||||
private UserInChallengeRepository $userInChallengeRepository;
|
||||
|
||||
private PlaceInChallengeRepository $placeInChallengeRepository;
|
||||
|
||||
private GuessRepository $guessRepository;
|
||||
|
||||
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->mapRepository = new MapRepository();
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
$this->challengeRepository = new ChallengeRepository();
|
||||
$this->userInChallengeRepository = new UserInChallengeRepository();
|
||||
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
|
||||
$this->guessRepository = new GuessRepository();
|
||||
}
|
||||
|
||||
public function getNewPlace(): IContent
|
||||
public function authorize(): bool
|
||||
{
|
||||
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
|
||||
}
|
||||
|
||||
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 (count($state['rounds']) === 0) {
|
||||
$placesWithoutPano = [];
|
||||
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
|
||||
|
||||
$this->addNewRoundToState($state, $place, $placesWithoutPano);
|
||||
|
||||
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
|
||||
$this->startNewGame($state, $mapId);
|
||||
$session->set('state', $state);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'panoId' => $place->getPanoIdCached(),
|
||||
'pov' => $place->getPov()->toArray()
|
||||
];
|
||||
} else {
|
||||
$rounds = count($state['rounds']);
|
||||
$last = $state['rounds'][$rounds - 1];
|
||||
$response = [];
|
||||
|
||||
$history = [];
|
||||
for ($i = 0; $i < $rounds - 1; ++$i) {
|
||||
$round = $state['rounds'][$i];
|
||||
$history[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
$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']
|
||||
];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'history' => $history,
|
||||
'panoId' => $last['panoId'],
|
||||
'pov' => isset($last['pov']) ? // should be checked not to break with old sessions
|
||||
$last['pov']->toArray() :
|
||||
['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonContent($data);
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
public function evaluateGuess(): IContent
|
||||
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]);
|
||||
}
|
||||
|
||||
private function prepareChallengeResponse(int $userId, Challenge $challenge, int $currentRound, bool $withHistory = false): array
|
||||
{
|
||||
$currentPlace = $this->placeRepository->getByRoundInChallenge($challenge, $currentRound);
|
||||
|
||||
// if the last round was played ($currentPlace == null) or history is explicitly requested (for initializing)
|
||||
if (!isset($currentPlace) || $withHistory) {
|
||||
|
||||
$withRelations = [User::class, PlaceInChallenge::class, Place::class];
|
||||
foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) {
|
||||
$round = $guess->getPlaceInChallenge()->getRound();
|
||||
|
||||
if ($guess->getUser()->getId() === $userId) {
|
||||
$response['history'][$round]['position'] =
|
||||
$guess->getPlaceInChallenge()->getPlace()->getPosition()->toArray();
|
||||
$response['history'][$round]['result'] = [
|
||||
'guessPosition' => $guess->getPosition()->toArray(),
|
||||
'distance' => $guess->getDistance(),
|
||||
'score' => $guess->getScore()
|
||||
];
|
||||
} else {
|
||||
$response['history'][$round]['allResults'][] = [
|
||||
'userName' => $guess->getUser()->getDisplayName(),
|
||||
'guessPosition' => $guess->getPosition()->toArray(),
|
||||
'distance' => $guess->getDistance(),
|
||||
'score' => $guess->getScore()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// setting default values for rounds without guesses (because of timeout)
|
||||
for ($i = 0; $i < $currentRound; ++$i) {
|
||||
if (!isset($response['history'][$i]) || !isset($response['history'][$i]['result'])) {
|
||||
$response['history'][$i]['result'] = [
|
||||
'guessPosition' => null,
|
||||
'distance' => null,
|
||||
'score' => 0
|
||||
];
|
||||
|
||||
$response['history'][$i]['position'] =
|
||||
$this->placeRepository->getByRoundInChallenge($challenge, $i)->getPosition()->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
$response['history']['length'] = $currentRound;
|
||||
}
|
||||
|
||||
if (!isset($currentPlace)) { // game finished
|
||||
$response['finished'] = true;
|
||||
} else { // continue game
|
||||
$response['place'] = [
|
||||
'panoId' => $currentPlace->getPanoIdCached(),
|
||||
'pov' => $currentPlace->getPov()->toArray()
|
||||
];
|
||||
|
||||
$prevRound = $currentRound - 1;
|
||||
if ($prevRound >= 0) {
|
||||
foreach ($this->guessRepository->getAllInChallengeByRound($prevRound, $challenge, [User::class]) as $guess) {
|
||||
if ($guess->getUser()->getId() != $userId) {
|
||||
$response['allResults'][] = [
|
||||
'userName' => $guess->getUser()->getDisplayName(),
|
||||
'guessPosition' => $guess->getPosition()->toArray(),
|
||||
'distance' => $guess->getDistance(),
|
||||
'score' => $guess->getScore()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response['restrictions'] = [
|
||||
'timeLimit' => $challenge->getTimeLimit() * 1000,
|
||||
'timeLimitType' => $challenge->getTimeLimitType(),
|
||||
'noMove' => $challenge->getNoMove(),
|
||||
'noPan' => $challenge->getNoPan(),
|
||||
'noZoom' => $challenge->getNoZoom()
|
||||
];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function challengeInitialData(): IContent
|
||||
{
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
$challengeToken_str = $this->request->query('challengeToken');
|
||||
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, [Challenge::class]);
|
||||
|
||||
if (!isset($userInChallenge)) {
|
||||
return new JsonContent(['error' => 'game_not_found']);
|
||||
}
|
||||
|
||||
$challenge = $userInChallenge->getChallenge();
|
||||
$currentRound = $userInChallenge->getCurrentRound();
|
||||
|
||||
$response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true);
|
||||
|
||||
if ($challenge->getTimeLimitType() === 'game' && $challenge->getTimeLimit() !== null && $userInChallenge->getCurrentRound() > 0) {
|
||||
$timeLimit = max(10, $userInChallenge->getTimeLeft());
|
||||
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
|
||||
}
|
||||
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
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'][count($state['rounds']) - 1];
|
||||
|
||||
$position = $last['position'];
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
|
||||
$distance = $this->calculateDistance($position, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $state['area']);
|
||||
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
$last['distance'] = $distance;
|
||||
$last['score'] = $score;
|
||||
$state['rounds'][count($state['rounds']) - 1] = $last;
|
||||
$last['distance'] = $result['distance'];
|
||||
$last['score'] = $result['score'];
|
||||
|
||||
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
|
||||
$exclude = [];
|
||||
$response = [
|
||||
'position' => $last['position']->toArray(),
|
||||
'result' => $result
|
||||
];
|
||||
|
||||
foreach ($state['rounds'] as $round) {
|
||||
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
|
||||
}
|
||||
$state['rounds'][$state['currentRound']] = $last;
|
||||
$state['currentRound'] += 1;
|
||||
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
|
||||
$next = $state['rounds'][$state['currentRound']];
|
||||
|
||||
$placesWithoutPano = [];
|
||||
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
|
||||
|
||||
$this->addNewRoundToState($state, $place, $placesWithoutPano);
|
||||
|
||||
$panoId = $place->getPanoIdCached();
|
||||
$pov = $place->getPov()->toArray();
|
||||
} else {
|
||||
$state['rounds'] = [];
|
||||
|
||||
$panoId = null;
|
||||
$pov = null;
|
||||
$response['place'] = [
|
||||
'panoId' => $next['panoId'],
|
||||
'pov' => $next['pov']->toArray()
|
||||
];
|
||||
}
|
||||
|
||||
$session->set('state', $state);
|
||||
|
||||
return new JsonContent([
|
||||
'result' => [
|
||||
'position' => $position->toArray(),
|
||||
'distance' => $distance,
|
||||
'score' => $score
|
||||
],
|
||||
'panoId' => $panoId,
|
||||
'pov' => $pov
|
||||
]);
|
||||
$this->saveVisit($last['placeId']);
|
||||
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
|
||||
// save the selected place for the round in UserPlayedPlace
|
||||
private function saveVisit($placeId): void
|
||||
{
|
||||
$state['rounds'][] = [
|
||||
'placesWithoutPano' => $placesWithoutPano,
|
||||
'placeId' => $place->getId(),
|
||||
'position' => $place->getPosition(),
|
||||
'panoId' => $place->getPanoIdCached(),
|
||||
'pov' => $place->getPov()
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
|
||||
if (isset($userId)) {
|
||||
$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->evaluateGuess($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 challengeGuess(): IContent
|
||||
{
|
||||
$session = $this->request->session();
|
||||
$userId = $session->get('userId');
|
||||
$challengeToken_str = $this->request->query('challengeToken');
|
||||
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, [Challenge::class]);
|
||||
|
||||
if (!isset($userInChallenge)) {
|
||||
return new JsonContent(['error' => 'game_not_found']);
|
||||
}
|
||||
|
||||
$challenge = $userInChallenge->getChallenge();
|
||||
$currentRound = $userInChallenge->getCurrentRound();
|
||||
$currentPlaceInChallenge = $this->placeInChallengeRepository->getByRoundInChallenge($currentRound, $challenge, [Place::class, Map::class]);
|
||||
$currentPlace = $currentPlaceInChallenge->getPlace();
|
||||
$map = $currentPlace->getMap();
|
||||
|
||||
// creating response
|
||||
$nextRound = $currentRound + 1;
|
||||
$response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
|
||||
$response['position'] = $currentPlace->getPosition()->toArray();
|
||||
|
||||
if ($this->request->post('lat') && $this->request->post('lng')) {
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
$result = $this->evaluateGuess($currentPlace->getPosition(), $guessPosition, $map->getArea());
|
||||
|
||||
// save guess
|
||||
$guess = new Guess();
|
||||
$guess->setUserId($userId);
|
||||
$guess->setPlaceInChallenge($currentPlaceInChallenge);
|
||||
$guess->setPosition($guessPosition);
|
||||
$guess->setDistance($result['distance']);
|
||||
$guess->setScore($result['score']);
|
||||
$this->pdm->saveToDb($guess);
|
||||
|
||||
$response['result'] = $result;
|
||||
|
||||
} else {
|
||||
// user didn't manage to guess in the round in the given timeframe
|
||||
$response['result'] = ['distance' => null, 'score' => 0];
|
||||
}
|
||||
|
||||
// save user relevant state of challenge
|
||||
$userInChallenge->setCurrentRound($nextRound);
|
||||
$timeLeft = $this->request->post('timeLeft');
|
||||
if (isset($timeLeft)) {
|
||||
$userInChallenge->setTimeLeft(intval($timeLeft));
|
||||
}
|
||||
$this->pdm->saveToDb($userInChallenge);
|
||||
|
||||
if ($challenge->getTimeLimitType() === 'game' && isset($timeLeft)) {
|
||||
$timeLimit = max(10, intval($timeLeft));
|
||||
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
|
||||
}
|
||||
|
||||
if (isset($response['history'][$currentRound]['allResults'])) {
|
||||
$response['allResults'] = $response['history'][$currentRound]['allResults'];
|
||||
}
|
||||
|
||||
$this->saveVisit($currentPlace->getId());
|
||||
|
||||
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 evaluateGuess(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
|
||||
|
||||
@@ -14,10 +14,12 @@ 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\CaptchaValidator;
|
||||
use MapGuesser\Util\JwtParser;
|
||||
|
||||
class LoginController
|
||||
@@ -32,6 +34,8 @@ class LoginController
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
@@ -39,6 +43,7 @@ class LoginController
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
}
|
||||
|
||||
public function getLoginForm()
|
||||
@@ -281,6 +286,18 @@ class LoginController
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
|
||||
if (!$this->request->post('g-recaptcha-response')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
|
||||
}
|
||||
|
||||
$captchaValidator = new CaptchaValidator();
|
||||
$captchaResponse = $captchaValidator->validate($this->request->post('g-recaptcha-response'));
|
||||
if (!$captchaResponse['success']) {
|
||||
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
|
||||
}
|
||||
}
|
||||
|
||||
if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
|
||||
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
|
||||
}
|
||||
@@ -430,6 +447,10 @@ class LoginController
|
||||
|
||||
$user = $this->userRepository->getById($confirmation->getUserId());
|
||||
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
@@ -447,6 +468,18 @@ class LoginController
|
||||
]);
|
||||
}
|
||||
|
||||
if (!empty($_ENV['RECAPTCHA_SITEKEY'])) {
|
||||
if (!$this->request->post('g-recaptcha-response')) {
|
||||
return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]);
|
||||
}
|
||||
|
||||
$captchaValidator = new CaptchaValidator();
|
||||
$captchaResponse = $captchaValidator->validate($this->request->post('g-recaptcha-response'));
|
||||
if (!$captchaResponse['success']) {
|
||||
return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]);
|
||||
}
|
||||
}
|
||||
|
||||
$user = $this->userRepository->getByEmail($this->request->post('email'));
|
||||
|
||||
if ($user === null) {
|
||||
|
||||
@@ -5,11 +5,18 @@ use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\PlaceInChallenge;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\ChallengeRepository;
|
||||
use MapGuesser\Repository\GuessRepository;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\PlaceInChallengeRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Repository\UserInChallengeRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
@@ -27,12 +34,27 @@ class MapAdminController implements ISecured
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
private ChallengeRepository $challengeRepository;
|
||||
|
||||
private GuessRepository $guessRepository;
|
||||
|
||||
private PlaceInChallengeRepository $placeInChallengeRepository;
|
||||
|
||||
private UserInChallengeRepository $userInChallengeRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->mapRepository = new MapRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
$this->challengeRepository = new ChallengeRepository();
|
||||
$this->guessRepository = new GuessRepository();
|
||||
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
|
||||
$this->userInChallengeRepository = new UserInChallengeRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
@@ -59,6 +81,7 @@ class MapAdminController implements ISecured
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
|
||||
'mapUnlisted' => $map->getUnlisted(),
|
||||
'bounds' => $map->getBounds()->toArray(),
|
||||
'places' => &$places
|
||||
]);
|
||||
@@ -138,7 +161,7 @@ class MapAdminController implements ISecured
|
||||
|
||||
$place = $this->placeRepository->getById((int) $placeRaw['id']);
|
||||
|
||||
$this->pdm->deleteFromDb($place);
|
||||
$this->deletePlace($place);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +176,9 @@ class MapAdminController implements ISecured
|
||||
if (isset($_POST['description'])) {
|
||||
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
|
||||
}
|
||||
if (isset($_POST['unlisted'])) {
|
||||
$map->setUnlisted((bool)$_POST['unlisted']);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($map);
|
||||
|
||||
@@ -161,7 +187,7 @@ class MapAdminController implements ISecured
|
||||
return new JsonContent(['mapId' => $map->getId(), 'added' => $addedIds]);
|
||||
}
|
||||
|
||||
public function deleteMap()
|
||||
public function deleteMap(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
@@ -178,13 +204,43 @@ class MapAdminController implements ISecured
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function deletePlace(Place $place): void
|
||||
{
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
foreach ($this->challengeRepository->getAllByPlace($place) as $challenge) {
|
||||
$this->deleteChallenge($challenge);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($place);
|
||||
}
|
||||
|
||||
private function deletePlaces(Map $map): void
|
||||
{
|
||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||
$this->pdm->deleteFromDb($place);
|
||||
$this->deletePlace($place);
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteChallenge(Challenge $challenge): void
|
||||
{
|
||||
foreach ($this->userInChallengeRepository->getAllByChallenge($challenge) as $userInChallenge) {
|
||||
$this->pdm->deleteFromDb($userInChallenge);
|
||||
}
|
||||
|
||||
foreach ($this->guessRepository->getAllInChallenge($challenge, [PlaceInChallenge::class]) as $guess) {
|
||||
$this->pdm->deleteFromDb($guess);
|
||||
}
|
||||
|
||||
foreach ($this->placeInChallengeRepository->getAllByChallenge($challenge) as $placeInChallenge) {
|
||||
$this->pdm->deleteFromDb($placeInChallenge);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($challenge);
|
||||
}
|
||||
|
||||
private function calculateMapBounds(Map $map): Bounds
|
||||
{
|
||||
$bounds = new Bounds();
|
||||
|
||||
@@ -30,12 +30,19 @@ class MapsController
|
||||
['maps', 'bound_north_lat'],
|
||||
['maps', 'bound_east_lng'],
|
||||
['maps', 'area'],
|
||||
['maps', 'unlisted'],
|
||||
new RawExpression('COUNT(places.id) AS num_places')
|
||||
]);
|
||||
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
|
||||
$select->groupBy(['maps', 'id']);
|
||||
$select->orderBy('name');
|
||||
|
||||
$user = $this->request->user();
|
||||
$isAdmin = $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
|
||||
if (!$isAdmin) {
|
||||
$select->where(['maps', 'unlisted'], '=', false);
|
||||
}
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
$maps = [];
|
||||
@@ -45,10 +52,10 @@ class MapsController
|
||||
$maps[] = $map;
|
||||
}
|
||||
|
||||
$user = $this->request->user();
|
||||
return new HtmlContent('maps', [
|
||||
'maps' => $maps,
|
||||
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
|
||||
'isLoggedIn' => $user !== null,
|
||||
'isAdmin' => $isAdmin
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@ use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\OAuth\GoogleOAuth;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserInChallenge;
|
||||
use MapGuesser\Repository\GuessRepository;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserInChallengeRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Response\Redirect;
|
||||
@@ -26,12 +30,21 @@ class UserController implements ISecured
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||
|
||||
private UserInChallengeRepository $userInChallengeRepository;
|
||||
|
||||
private GuessRepository $guessRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||
$this->userInChallengeRepository = new UserInChallengeRepository();
|
||||
$this->guessRepository = new GuessRepository();
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
@@ -201,6 +214,18 @@ class UserController implements ISecured
|
||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||
}
|
||||
|
||||
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||
}
|
||||
|
||||
foreach ($this->userInChallengeRepository->getAllByUser($user) as $userInChallenge) {
|
||||
$this->pdm->deleteFromDb($userInChallenge);
|
||||
}
|
||||
|
||||
foreach ($this->guessRepository->getAllByUser($user) as $guess) {
|
||||
$this->pdm->deleteFromDb($guess);
|
||||
}
|
||||
|
||||
$this->pdm->deleteFromDb($user);
|
||||
|
||||
\Container::$dbConnection->commit();
|
||||
@@ -210,7 +235,7 @@ class UserController implements ISecured
|
||||
return new JsonContent(['success' => true]);
|
||||
}
|
||||
|
||||
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, &$error): bool
|
||||
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool
|
||||
{
|
||||
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
|
||||
return true;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -12,6 +12,8 @@ class Select
|
||||
|
||||
const CONDITION_HAVING = 1;
|
||||
|
||||
const DERIVED_TABLE_KEY = 'DERIVED';
|
||||
|
||||
private IConnection $connection;
|
||||
|
||||
private string $table;
|
||||
@@ -30,7 +32,7 @@ class Select
|
||||
|
||||
private array $orders = [];
|
||||
|
||||
private array $limit;
|
||||
private ?array $limit;
|
||||
|
||||
public function __construct(IConnection $connection, ?string $table = null)
|
||||
{
|
||||
@@ -55,6 +57,11 @@ class Select
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDerivedTableAlias(string $tableAlias): Select
|
||||
{
|
||||
return $this->setTableAliases([Select::DERIVED_TABLE_KEY => $tableAlias]);
|
||||
}
|
||||
|
||||
public function from(string $table): Select
|
||||
{
|
||||
$this->table = $table;
|
||||
@@ -144,7 +151,7 @@ class Select
|
||||
$this->limit = null;
|
||||
}
|
||||
|
||||
public function paginate(int $page, int $itemsPerPage)
|
||||
public function paginate(int $page, int $itemsPerPage): Select
|
||||
{
|
||||
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
|
||||
|
||||
@@ -194,6 +201,11 @@ class Select
|
||||
}
|
||||
}
|
||||
|
||||
private function isDerivedTable(): bool
|
||||
{
|
||||
return array_key_exists(Select::DERIVED_TABLE_KEY, $this->tableAliases);
|
||||
}
|
||||
|
||||
private function addJoin(string $type, $table, $column1, string $relation, $column2): void
|
||||
{
|
||||
$this->joins[] = [$type, $table, $column1, $relation, $column2];
|
||||
@@ -211,10 +223,14 @@ class Select
|
||||
|
||||
private function generateQuery(): array
|
||||
{
|
||||
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $this->generateTable($this->table, true);
|
||||
list($tableQuery, $tableParams) = $this->generateTable($this->table, true);
|
||||
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $tableQuery;
|
||||
|
||||
if (count($this->joins) > 0) {
|
||||
$queryString .= ' ' . $this->generateJoins();
|
||||
list($joinQuery, $joinParams) = $this->generateJoins();
|
||||
$queryString .= ' ' . $joinQuery;
|
||||
} else {
|
||||
$joinParams = [];
|
||||
}
|
||||
|
||||
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
|
||||
@@ -245,20 +261,32 @@ class Select
|
||||
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
|
||||
}
|
||||
|
||||
return [$queryString, array_merge($whereParams, $havingParams)];
|
||||
if ($this->isDerivedTable()) {
|
||||
$queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY];
|
||||
}
|
||||
|
||||
return [$queryString, array_merge($tableParams, $joinParams, $whereParams, $havingParams)];
|
||||
}
|
||||
|
||||
private function generateTable($table, bool $defineAlias = false): string
|
||||
private function generateTable($table, bool $defineAlias = false): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
if ($table instanceof RawExpression) {
|
||||
return (string) $table;
|
||||
return [(string) $table, $params];
|
||||
}
|
||||
|
||||
if ($table instanceof Select)
|
||||
{
|
||||
return $table->generateQuery();
|
||||
}
|
||||
|
||||
if (isset($this->tableAliases[$table])) {
|
||||
return ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
|
||||
$queryString = ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
|
||||
return [$queryString, $params];
|
||||
}
|
||||
|
||||
return Utils::backtick($table);
|
||||
return [Utils::backtick($table), $params];
|
||||
}
|
||||
|
||||
private function generateColumn($column): string
|
||||
@@ -271,7 +299,8 @@ class Select
|
||||
$out = '';
|
||||
|
||||
if ($column[0]) {
|
||||
$out .= $this->generateTable($column[0]) . '.';
|
||||
list($tableName, $params) = $this->generateTable($column[0]);
|
||||
$out .= $tableName . '.';
|
||||
}
|
||||
|
||||
$out .= Utils::backtick($column[1]);
|
||||
@@ -297,18 +326,22 @@ class Select
|
||||
return implode(',', $columns);
|
||||
}
|
||||
|
||||
private function generateJoins(): string
|
||||
private function generateJoins(): array
|
||||
{
|
||||
$joins = $this->joins;
|
||||
|
||||
array_walk($joins, function (&$value, $key) {
|
||||
$value = $value[0] . ' JOIN ' . $this->generateTable($value[1], true) . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]);
|
||||
});
|
||||
$joinQueries = [];
|
||||
$params = [];
|
||||
|
||||
return implode(' ', $joins);
|
||||
foreach ($this->joins as $join) {
|
||||
list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true);
|
||||
$joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]);
|
||||
$params = array_merge($params, $paramsFragment);
|
||||
}
|
||||
|
||||
return [implode(' ', $joinQueries), $params];
|
||||
}
|
||||
|
||||
private function generateConditions(string $type): array
|
||||
private function generateConditions(int $type): array
|
||||
{
|
||||
$conditions = '';
|
||||
$params = [];
|
||||
@@ -376,9 +409,9 @@ class Select
|
||||
return [$conditionsString, $params];
|
||||
}
|
||||
|
||||
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array
|
||||
private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array
|
||||
{
|
||||
$instance = new static($this->connection, $this->table);
|
||||
$instance = new self($this->connection, $this->table);
|
||||
$instance->tableAliases = $this->tableAliases;
|
||||
|
||||
$conditionCallback($instance);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php namespace MapGuesser\Database;
|
||||
|
||||
class Utils {
|
||||
public static function backtick(string $name) {
|
||||
public static function backtick(string $name): string
|
||||
{
|
||||
return '`' . $name . '`';
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class Request implements IRequest
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
public function setQuery($query)
|
||||
public function setQuery($query): void
|
||||
{
|
||||
if (is_string($query)) {
|
||||
$this->query = $query;
|
||||
@@ -38,7 +38,7 @@ class Request implements IRequest
|
||||
}
|
||||
}
|
||||
|
||||
public function setHeaders(array $headers)
|
||||
public function setHeaders(array $headers): void
|
||||
{
|
||||
$this->headers = array_merge($this->headers, $headers);
|
||||
}
|
||||
@@ -47,13 +47,13 @@ class Request implements IRequest
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
if ($this->method === self::HTTP_GET) {
|
||||
$url = $this->url . '?' . $this->query;
|
||||
} elseif ($this->method === self::HTTP_POST) {
|
||||
$url = $this->url;
|
||||
if ($this->method === self::HTTP_POST) {
|
||||
$url = $this->url;
|
||||
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
|
||||
} else {
|
||||
$url = $this->url . '?' . $this->query;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
|
||||
@@ -14,12 +14,12 @@ class Response implements IResponse
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
public function getBody()
|
||||
public function getBody(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function getHeaders()
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ interface IRequest
|
||||
|
||||
public function setMethod(int $method): void;
|
||||
|
||||
public function setQuery($query);
|
||||
public function setQuery($query): void;
|
||||
|
||||
public function setHeaders(array $headers);
|
||||
public function setHeaders(array $headers): void;
|
||||
|
||||
public function send(): IResponse;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
interface IResponse
|
||||
{
|
||||
public function getBody();
|
||||
public function getBody(): string;
|
||||
|
||||
public function getHeaders();
|
||||
public function getHeaders(): array;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use MapGuesser\Interfaces\Authentication\IUser;
|
||||
|
||||
interface IRequest
|
||||
{
|
||||
public function setParsedRouteParams(array &$routeParams);
|
||||
public function setParsedRouteParams(array &$routeParams): void;
|
||||
|
||||
public function getBase(): string;
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class Mail
|
||||
}
|
||||
}
|
||||
|
||||
private function sendMail(PHPMailer $mailer, array $recipient)
|
||||
private function sendMail(PHPMailer $mailer, array $recipient): void
|
||||
{
|
||||
$mailer->clearAddresses();
|
||||
$mailer->addAddress($recipient[0], $recipient[1]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php namespace MapGuesser\Multi;
|
||||
|
||||
class MultiConnector
|
||||
{
|
||||
public function sendMessage(string $func, array $args = [])
|
||||
{
|
||||
$message = json_encode([
|
||||
'func' => $func,
|
||||
'args' => $args
|
||||
]);
|
||||
|
||||
$connection = fsockopen($_ENV['MULTI_INTERNAL_HOST'], $_ENV['MULTI_INTERNAL_PORT']);
|
||||
fwrite($connection, $message);
|
||||
$response = '';
|
||||
while (!feof($connection)) {
|
||||
$response .= fgets($connection);
|
||||
}
|
||||
fclose($connection);
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ use MapGuesser\Interfaces\Http\IRequest;
|
||||
|
||||
class GoogleOAuth
|
||||
{
|
||||
private static $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
private static string $dialogUrlBase = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
|
||||
private static $tokenUrlBase = 'https://oauth2.googleapis.com/token';
|
||||
private static string $tokenUrlBase = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
@@ -36,7 +36,7 @@ class GoogleOAuth
|
||||
return self::$dialogUrlBase . '?' . http_build_query($oauthParams);
|
||||
}
|
||||
|
||||
public function getToken(string $code, string $redirectUrl)
|
||||
public function getToken(string $code, string $redirectUrl): array
|
||||
{
|
||||
$tokenParams = [
|
||||
'code' => $code,
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class Challenge extends Model
|
||||
{
|
||||
protected static string $table = 'challenges';
|
||||
|
||||
protected static array $fields = ['token', 'time_limit', 'time_limit_type', 'no_move', 'no_pan', 'no_zoom', 'created'];
|
||||
|
||||
protected static array $relations = [];
|
||||
|
||||
private int $token;
|
||||
|
||||
private ?int $timeLimit = null;
|
||||
|
||||
private static array $timeLimitTypes = ['game', 'round'];
|
||||
|
||||
private string $timeLimitType = 'game';
|
||||
|
||||
private bool $noMove = false;
|
||||
|
||||
private bool $noPan = false;
|
||||
|
||||
private bool $noZoom = false;
|
||||
|
||||
private DateTime $created;
|
||||
|
||||
public function setToken(int $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function setTimeLimit(?int $timeLimit): void
|
||||
{
|
||||
if (isset($timeLimit)) {
|
||||
$this->timeLimit = $timeLimit;
|
||||
}
|
||||
}
|
||||
|
||||
public function setTimeLimitType(string $timeLimitType): void
|
||||
{
|
||||
if (in_array($timeLimitType, self::$timeLimitTypes)) {
|
||||
$this->timeLimitType = $timeLimitType;
|
||||
}
|
||||
}
|
||||
|
||||
public function setNoMove(bool $noMove): void
|
||||
{
|
||||
$this->noMove = $noMove;
|
||||
}
|
||||
|
||||
public function setNoPan(bool $noPan): void
|
||||
{
|
||||
$this->noPan = $noPan;
|
||||
}
|
||||
|
||||
public function setNoZoom(bool $noZoom): void
|
||||
{
|
||||
$this->noZoom = $noZoom;
|
||||
}
|
||||
|
||||
public function setCreatedDate(DateTime $created): void
|
||||
{
|
||||
$this->created = $created;
|
||||
}
|
||||
|
||||
public function setCreated(string $created): void
|
||||
{
|
||||
$this->created = new DateTime($created);
|
||||
}
|
||||
|
||||
public function getToken(): int
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function getTimeLimit(): ?int
|
||||
{
|
||||
return $this->timeLimit;
|
||||
}
|
||||
|
||||
public function getTimeLimitType(): string
|
||||
{
|
||||
return $this->timeLimitType;
|
||||
}
|
||||
|
||||
public function getNoMove(): bool
|
||||
{
|
||||
return $this->noMove;
|
||||
}
|
||||
|
||||
public function getNoPan(): bool
|
||||
{
|
||||
return $this->noPan;
|
||||
}
|
||||
|
||||
public function getNoZoom(): bool
|
||||
{
|
||||
return $this->noZoom;
|
||||
}
|
||||
|
||||
public function getCreatedDate(): DateTime
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function getCreated(): string
|
||||
{
|
||||
return $this->created->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
|
||||
class Guess extends Model
|
||||
{
|
||||
protected static string $table = 'guesses';
|
||||
|
||||
protected static array $fields = ['user_id', 'place_in_challenge_id', 'lat', 'lng', 'score', 'distance', 'time_spent'];
|
||||
|
||||
protected static array $relations = ['user' => User::class, 'place_in_challenge' => PlaceInChallenge::class];
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
private ?PlaceInChallenge $placeInChallenge = null;
|
||||
|
||||
private ?int $placeInChallengeId = null;
|
||||
|
||||
private Position $position;
|
||||
|
||||
private int $score = 0;
|
||||
|
||||
private int $distance = 0;
|
||||
|
||||
private int $timeSpent = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->position = new Position(0.0, 0.0);
|
||||
}
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setPlaceInChallenge(PlaceInChallenge $placeInChallenge): void
|
||||
{
|
||||
$this->placeInChallenge = $placeInChallenge;
|
||||
}
|
||||
|
||||
public function setPlaceInChallengeId(int $placeInChallengeId): void
|
||||
{
|
||||
$this->placeInChallengeId = $placeInChallengeId;
|
||||
}
|
||||
|
||||
public function setPosition(Position $position): void
|
||||
{
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
public function setLat(float $lat): void
|
||||
{
|
||||
$this->position->setLat($lat);
|
||||
}
|
||||
|
||||
public function setLng(float $lng): void
|
||||
{
|
||||
$this->position->setLng($lng);
|
||||
}
|
||||
|
||||
public function setScore(int $score): void
|
||||
{
|
||||
$this->score = $score;
|
||||
}
|
||||
|
||||
public function setDistance(int $distance): void
|
||||
{
|
||||
$this->distance = $distance;
|
||||
}
|
||||
|
||||
public function setTimeSpent(int $timeSpent): void
|
||||
{
|
||||
$this->timeSpent = $timeSpent;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getPlaceInChallenge(): ?PlaceInChallenge
|
||||
{
|
||||
return $this->placeInChallenge;
|
||||
}
|
||||
|
||||
public function getPlaceInChallengeId(): ?int
|
||||
{
|
||||
return $this->placeInChallengeId;
|
||||
}
|
||||
|
||||
public function getPosition(): Position
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function getLat(): float
|
||||
{
|
||||
return $this->position->getLat();
|
||||
}
|
||||
|
||||
public function getLng(): float
|
||||
{
|
||||
return $this->position->getLng();
|
||||
}
|
||||
|
||||
public function getScore(): int
|
||||
{
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
public function getDistance(): int
|
||||
{
|
||||
return $this->distance;
|
||||
}
|
||||
|
||||
public function getTimeSpent(): ?int
|
||||
{
|
||||
return $this->timeSpent;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ class Map extends Model
|
||||
{
|
||||
protected static string $table = 'maps';
|
||||
|
||||
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area'];
|
||||
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area', 'unlisted'];
|
||||
|
||||
private string $name = '';
|
||||
|
||||
@@ -16,6 +16,8 @@ class Map extends Model
|
||||
|
||||
private float $area = 0.0;
|
||||
|
||||
private bool $unlisted = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
|
||||
@@ -61,6 +63,11 @@ class Map extends Model
|
||||
$this->area = $area;
|
||||
}
|
||||
|
||||
public function setUnlisted(bool $unlisted): void
|
||||
{
|
||||
$this->unlisted = $unlisted;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
@@ -100,4 +107,9 @@ class Map extends Model
|
||||
{
|
||||
return $this->area;
|
||||
}
|
||||
|
||||
public function getUnlisted(): bool
|
||||
{
|
||||
return $this->unlisted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class MultiRoom extends Model
|
||||
{
|
||||
protected static string $table = 'multi_rooms';
|
||||
|
||||
protected static array $fields = ['room_id', 'state', 'members', 'updated'];
|
||||
|
||||
private string $roomId = '';
|
||||
|
||||
private array $state = [];
|
||||
|
||||
private array $members = [];
|
||||
|
||||
private DateTime $updated;
|
||||
|
||||
public function setRoomId(string $roomId): void
|
||||
{
|
||||
$this->roomId = $roomId;
|
||||
}
|
||||
|
||||
public function setStateArray(array $state): void
|
||||
{
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
public function setMembersArray(array $members): void
|
||||
{
|
||||
$this->members = $members;
|
||||
}
|
||||
|
||||
public function setState(string $state): void
|
||||
{
|
||||
$this->state = unserialize($state);
|
||||
}
|
||||
|
||||
public function setMembers(string $members): void
|
||||
{
|
||||
$this->members = unserialize($members);
|
||||
}
|
||||
|
||||
public function setUpdatedDate(DateTime $updated): void
|
||||
{
|
||||
$this->updated = $updated;
|
||||
}
|
||||
|
||||
public function setUpdated(string $updated): void
|
||||
{
|
||||
$this->updated = new DateTime($updated);
|
||||
}
|
||||
|
||||
public function getRoomId(): string
|
||||
{
|
||||
return $this->roomId;
|
||||
}
|
||||
|
||||
public function getStateArray(): array
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getState(): string
|
||||
{
|
||||
return serialize($this->state);
|
||||
}
|
||||
|
||||
public function getMembersArray(): array
|
||||
{
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
public function getMembers(): string
|
||||
{
|
||||
return serialize($this->members);
|
||||
}
|
||||
|
||||
public function getUpdatedDate(): DateTime
|
||||
{
|
||||
return $this->updated;
|
||||
}
|
||||
|
||||
public function getUpdated(): string
|
||||
{
|
||||
return $this->updated->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class Place extends Model
|
||||
return $this->pov->getZoom();
|
||||
}
|
||||
|
||||
public function getFreshPanoId(bool $canBeIndoor = false): ?string
|
||||
public function getFreshPanoId(): ?string
|
||||
{
|
||||
if (
|
||||
$this->panoIdCachedTimestamp !== null &&
|
||||
@@ -152,19 +152,13 @@ class Place extends Model
|
||||
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
|
||||
$request->setQuery([
|
||||
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
|
||||
'location' => $this->position->getLat() . ',' . $this->position->getLng(),
|
||||
'source' => $canBeIndoor ? 'default' : 'outdoor'
|
||||
'location' => $this->position->getLat() . ',' . $this->position->getLng()
|
||||
]);
|
||||
|
||||
$response = $request->send();
|
||||
$panoData = json_decode($response->getBody(), true);
|
||||
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
|
||||
|
||||
// enable indoor panos if no outdoor found
|
||||
if ($panoId === null && !$canBeIndoor) {
|
||||
return $this->getFreshPanoId(true);
|
||||
}
|
||||
|
||||
$this->panoIdCached = $panoId;
|
||||
$this->panoIdCachedTimestamp = new DateTime();
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
class PlaceInChallenge extends Model
|
||||
{
|
||||
protected static string $table = 'place_in_challenge';
|
||||
|
||||
protected static array $fields = ['place_id', 'challenge_id', 'round'];
|
||||
|
||||
protected static array $relations = ['place' => Place::class, 'challenge' => Challenge::class];
|
||||
|
||||
private ?Place $place = null;
|
||||
|
||||
private ?int $placeId = null;
|
||||
|
||||
private ?Challenge $challenge = null;
|
||||
|
||||
private ?int $challengeId = null;
|
||||
|
||||
private int $round;
|
||||
|
||||
public function setPlace(Place $place): void
|
||||
{
|
||||
$this->place = $place;
|
||||
}
|
||||
|
||||
public function setPlaceId(int $placeId): void
|
||||
{
|
||||
$this->placeId = $placeId;
|
||||
}
|
||||
|
||||
public function setChallenge(Challenge $challenge): void
|
||||
{
|
||||
$this->challenge = $challenge;
|
||||
}
|
||||
|
||||
public function setChallengeId(int $challengeId): void
|
||||
{
|
||||
$this->challengeId = $challengeId;
|
||||
}
|
||||
|
||||
public function setRound(int $round): void
|
||||
{
|
||||
$this->round = $round;
|
||||
}
|
||||
|
||||
public function getPlace(): ?Place
|
||||
{
|
||||
return $this->place;
|
||||
}
|
||||
|
||||
public function getPlaceId(): ?int
|
||||
{
|
||||
return $this->placeId;
|
||||
}
|
||||
|
||||
public function getChallenge(): ?Challenge
|
||||
{
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
public function getChallengeId(): ?int
|
||||
{
|
||||
return $this->challengeId;
|
||||
}
|
||||
|
||||
public function getRound(): int
|
||||
{
|
||||
return $this->round;
|
||||
}
|
||||
}
|
||||
@@ -45,9 +45,9 @@ class User extends Model implements IUser
|
||||
}
|
||||
}
|
||||
|
||||
public function setActive($active): void
|
||||
public function setActive(bool $active): void
|
||||
{
|
||||
$this->active = (bool) $active;
|
||||
$this->active = $active;
|
||||
}
|
||||
|
||||
public function setGoogleSub(?string $googleSub): void
|
||||
@@ -105,10 +105,10 @@ class User extends Model implements IUser
|
||||
switch ($permission) {
|
||||
case IUser::PERMISSION_NORMAL:
|
||||
return true;
|
||||
break;
|
||||
case IUser::PERMISSION_ADMIN:
|
||||
return $this->type === 'admin';
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('Permission does not exist: ' . $permission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,10 @@ class User extends Model implements IUser
|
||||
|
||||
public function checkPassword(string $password): bool
|
||||
{
|
||||
if ($this->password === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return password_verify($password, $this->password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
class UserInChallenge extends Model
|
||||
{
|
||||
protected static string $table = 'user_in_challenge';
|
||||
|
||||
protected static array $fields = ['user_id', 'challenge_id', 'current_round', 'time_left', 'is_owner'];
|
||||
|
||||
protected static array $relations = ['user' => User::class, 'challenge' => Challenge::class];
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
private ?Challenge $challenge = null;
|
||||
|
||||
private ?int $challengeId = null;
|
||||
|
||||
private int $currentRound = 0;
|
||||
|
||||
private ?int $timeLeft = null;
|
||||
|
||||
private bool $isOwner = false;
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setChallenge(Challenge $challenge): void
|
||||
{
|
||||
$this->challenge = $challenge;
|
||||
}
|
||||
|
||||
public function setChallengeId(int $challengeId): void
|
||||
{
|
||||
$this->challengeId = $challengeId;
|
||||
}
|
||||
|
||||
public function setCurrentRound(int $currentRound): void
|
||||
{
|
||||
$this->currentRound = $currentRound;
|
||||
}
|
||||
|
||||
public function setTimeLeft(?int $timeLeft): void
|
||||
{
|
||||
if (isset($timeLeft)) {
|
||||
$this->timeLeft = max(0, $timeLeft);
|
||||
}
|
||||
}
|
||||
|
||||
public function setIsOwner(bool $isOwner): void
|
||||
{
|
||||
$this->isOwner = $isOwner;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getChallenge(): ?Challenge
|
||||
{
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
public function getChallengeId(): ?int
|
||||
{
|
||||
return $this->challengeId;
|
||||
}
|
||||
|
||||
public function getCurrentRound(): int
|
||||
{
|
||||
return $this->currentRound;
|
||||
}
|
||||
|
||||
public function getTimeLeft(): ?int
|
||||
{
|
||||
return $this->timeLeft;
|
||||
}
|
||||
|
||||
public function getIsOwner(): bool
|
||||
{
|
||||
return $this->isOwner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php namespace MapGuesser\PersistentData\Model;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class UserPlayedPlace extends Model
|
||||
{
|
||||
protected static string $table = 'user_played_place';
|
||||
|
||||
protected static array $fields = ['user_id', 'place_id', 'last_time', 'occurrences'];
|
||||
|
||||
protected static array $relations = ['user' => User::class, 'place' => Place::class];
|
||||
|
||||
private ?User $user = null;
|
||||
|
||||
private ?int $userId = null;
|
||||
|
||||
private ?Place $place = null;
|
||||
|
||||
private ?int $placeId = null;
|
||||
|
||||
private DateTime $lastTime;
|
||||
|
||||
private int $occurrences = 1;
|
||||
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function setPlace(Place $place): void
|
||||
{
|
||||
$this->place = $place;
|
||||
}
|
||||
|
||||
public function setPlaceId(int $placeId): void
|
||||
{
|
||||
$this->placeId = $placeId;
|
||||
}
|
||||
|
||||
public function setLastTimeDate(DateTime $lastTime): void
|
||||
{
|
||||
$this->lastTime = $lastTime;
|
||||
}
|
||||
|
||||
public function setLastTime(string $lastTime): void
|
||||
{
|
||||
$this->lastTime = new DateTime($lastTime);
|
||||
}
|
||||
|
||||
public function setOccurrences(int $occurrences): void
|
||||
{
|
||||
$this->occurrences = $occurrences;
|
||||
}
|
||||
|
||||
public function incrementOccurrences(): void
|
||||
{
|
||||
$this->occurrences++;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getPlace(): ?Place
|
||||
{
|
||||
return $this->place;
|
||||
}
|
||||
|
||||
public function getPlaceId(): ?int
|
||||
{
|
||||
return $this->placeId;
|
||||
}
|
||||
|
||||
public function getLastTimeDate(): DateTime
|
||||
{
|
||||
return $this->lastTime;
|
||||
}
|
||||
|
||||
public function getLastTime(): string
|
||||
{
|
||||
return $this->lastTime->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
public function getOccurrences(): int
|
||||
{
|
||||
return $this->occurrences;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ use MapGuesser\PersistentData\Model\Model;
|
||||
|
||||
class PersistentDataManager
|
||||
{
|
||||
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model
|
||||
public function selectFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = [])
|
||||
{
|
||||
$select = $this->createSelect($select, $type, $withRelations);
|
||||
$select = $this->createSelect($select, $type, $useRelations, $withRelations);
|
||||
|
||||
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
@@ -19,50 +19,75 @@ class PersistentDataManager
|
||||
}
|
||||
|
||||
$model = new $type();
|
||||
$this->fillWithData($data, $model);
|
||||
$this->fillWithData($data, $model, $withRelations);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator
|
||||
public function selectMultipleFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Generator
|
||||
{
|
||||
$select = $this->createSelect($select, $type, $withRelations);
|
||||
$select = $this->createSelect($select, $type, $useRelations, $withRelations);
|
||||
$result = $select->execute();
|
||||
|
||||
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$model = new $type();
|
||||
$this->fillWithData($data, $model);
|
||||
$this->fillWithData($data, $model, $withRelations);
|
||||
|
||||
yield $model;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model
|
||||
public function selectFromDbById($id, string $type, bool $useRelations = false)
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->whereId($id);
|
||||
|
||||
return $this->selectFromDb($select, $type, $withRelations);
|
||||
return $this->selectFromDb($select, $type, $useRelations);
|
||||
}
|
||||
|
||||
public function fillWithData(array $data, Model $model): void
|
||||
public function fillWithData(array &$data, Model $model, array $withRelations = [], ?string $modelKey = null): void
|
||||
{
|
||||
$relations = $model::getRelations();
|
||||
$relationData = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($this->extractRelationData($key, $value, $relationData, $relations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
|
||||
|
||||
if (method_exists($model, $method)) {
|
||||
$model->$method($value);
|
||||
}
|
||||
if (count($withRelations)) {
|
||||
$relations = array_intersect($relations, $withRelations);
|
||||
}
|
||||
|
||||
$this->setRelations($model, $relationData);
|
||||
while (key($data)) {
|
||||
$key = key($data);
|
||||
$value = current($data);
|
||||
$relation = key($relations);
|
||||
|
||||
if (strpos($key, '__') == false) {
|
||||
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
|
||||
|
||||
if (method_exists($model, $method) && isset($value)) {
|
||||
$model->$method($value);
|
||||
}
|
||||
|
||||
next($data);
|
||||
} else if (isset($modelKey) && substr($key, 0, strlen($modelKey . '__')) === $modelKey . '__') {
|
||||
$key = substr($key, strlen($modelKey) + 2);
|
||||
|
||||
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
|
||||
|
||||
if (method_exists($model, $method) && isset($value)) {
|
||||
$model->$method($value);
|
||||
}
|
||||
|
||||
next($data);
|
||||
} else if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
|
||||
$relationType = current($relations);
|
||||
$relationModel = new $relationType();
|
||||
$this->fillWithData($data, $relationModel, $withRelations, $relation);
|
||||
|
||||
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
|
||||
$model->$method($relationModel);
|
||||
|
||||
next($relations);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$model->saveSnapshot();
|
||||
}
|
||||
@@ -128,35 +153,37 @@ class PersistentDataManager
|
||||
$model->resetSnapshot();
|
||||
}
|
||||
|
||||
private function createSelect(Select $select, string $type, bool $withRelations = false): Select
|
||||
private function createSelect(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Select
|
||||
{
|
||||
$table = call_user_func([$type, 'getTable']);
|
||||
$fields = call_user_func([$type, 'getFields']);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$columns[] = [$table, $field];
|
||||
}
|
||||
|
||||
$select->from($table);
|
||||
|
||||
//TODO: only with some relations?
|
||||
if ($withRelations) {
|
||||
if ($useRelations) {
|
||||
$relations = call_user_func([$type, 'getRelations']);
|
||||
|
||||
$columns = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$columns[] = [$table, $field];
|
||||
if (count($withRelations)) {
|
||||
$relations = array_intersect($relations, $withRelations);
|
||||
}
|
||||
|
||||
$columns = array_merge($columns, $this->getRelationColumns($relations));
|
||||
$columns = array_merge($columns, $this->getRelationColumns($relations, $withRelations));
|
||||
|
||||
$this->leftJoinRelations($select, $table, $relations);
|
||||
$this->leftJoinRelations($select, $table, $relations, $withRelations);
|
||||
$select->columns($columns);
|
||||
} else {
|
||||
$select->columns($fields);
|
||||
$select->columns($columns);
|
||||
}
|
||||
|
||||
return $select;
|
||||
}
|
||||
|
||||
private function getRelationColumns(array $relations): array
|
||||
private function getRelationColumns(array $relations, array $withRelations): array
|
||||
{
|
||||
$columns = [];
|
||||
|
||||
@@ -165,46 +192,28 @@ class PersistentDataManager
|
||||
foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
|
||||
$columns[] = [$relationTable, $relationField, $relation . '__' . $relationField];
|
||||
}
|
||||
|
||||
$nextOrderRelations = call_user_func([$relationType, 'getRelations']);
|
||||
if (count($withRelations)) {
|
||||
$nextOrderRelations = array_intersect($nextOrderRelations, $withRelations);
|
||||
}
|
||||
$columns = array_merge($columns, $this->getRelationColumns($nextOrderRelations, $withRelations));
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
private function leftJoinRelations(Select $select, string $table, array $relations): void
|
||||
private function leftJoinRelations(Select $select, string $table, array $relations, array $withRelations): void
|
||||
{
|
||||
foreach ($relations as $relation => $relationType) {
|
||||
$relationTable = call_user_func([$relationType, 'getTable']);
|
||||
$select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool
|
||||
{
|
||||
$found = false;
|
||||
|
||||
foreach ($relations as $relation => $relationType) {
|
||||
if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
|
||||
$found = true;
|
||||
$relationData[$relation][substr($key, strlen($relation . '__'))] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
private function setRelations(Model $model, array &$relations): void
|
||||
{
|
||||
foreach ($model::getRelations() as $relation => $relationType) {
|
||||
if (isset($relations[$relation])) {
|
||||
$object = new $relationType();
|
||||
|
||||
$this->fillWithData($relations[$relation], $object);
|
||||
|
||||
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
|
||||
|
||||
$model->$method($object);
|
||||
$nextOrderRelations = call_user_func([$relationType, 'getRelations']);
|
||||
if (count($withRelations)) {
|
||||
$nextOrderRelations = array_intersect($nextOrderRelations, $withRelations);
|
||||
}
|
||||
$this->leftJoinRelations($select, $relationTable, $nextOrderRelations, $withRelations);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class ChallengeRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getById(int $challengeId): ?Challenge
|
||||
{
|
||||
return $this->pdm->selectFromDbById($challengeId, Challenge::class);
|
||||
}
|
||||
|
||||
public function getByToken(int $token): ?Challenge
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('token', '=', $token);
|
||||
|
||||
return $this->pdm->selectFromDb($select, Challenge::class);
|
||||
}
|
||||
|
||||
public function getByTokenStr(string $token_str): ?Challenge
|
||||
{
|
||||
// validate token string
|
||||
foreach (str_split($token_str) as $char) {
|
||||
if (!(('0' <= $char && $char <= '9') || ('a' <= $char && $char <= 'f'))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// convert token to int
|
||||
$token = hexdec($token_str);
|
||||
|
||||
return $this->getByToken($token);
|
||||
}
|
||||
|
||||
public function getAllByParticipant(User $user): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
|
||||
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Challenge::class);
|
||||
}
|
||||
|
||||
public function getAllByOwner(User $user): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']);
|
||||
$select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
$select->where('is_owner', '=', true);
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Challenge::class);
|
||||
}
|
||||
|
||||
public function getAllByPlace(Place $place): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['challenges', 'id'], '=', ['place_in_challenge', 'challenge_id']);
|
||||
$select->where('place_id', '=', $place->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Challenge::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Guess;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserInChallenge;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\PlaceInChallenge;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class GuessRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getAllByUser(User $user): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Guess::class);
|
||||
}
|
||||
|
||||
public function getAllByUserAndChallenge(User $user, Challenge $challenge): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Guess::class);
|
||||
}
|
||||
|
||||
public function getByUserAndPlaceInChallenge(User $user, Challenge $challenge, Place $place): ?Guess
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->where('place_id', '=', $place->getId());
|
||||
|
||||
return $this->pdm->selectFromDb($select, Guess::class);
|
||||
}
|
||||
|
||||
public function getAllInChallengeByUser(int $userId, Challenge $challenge): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
|
||||
$select->where('user_id', '=', $userId);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->orderBy('round');
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Guess::class);
|
||||
}
|
||||
|
||||
public function getAllInChallenge(Challenge $challenge, array $withRelations = []): Generator
|
||||
{
|
||||
if (count($withRelations)) {
|
||||
$necessaryRelations = [PlaceInChallenge::class];
|
||||
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
|
||||
}
|
||||
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->orderBy('round');
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Guess::class, true, $withRelations);
|
||||
}
|
||||
|
||||
public function getAllInChallengeByRound(int $round, Challenge $challenge, array $withRelations = []): Generator
|
||||
{
|
||||
if (count($withRelations)) {
|
||||
$necessaryRelations = [PlaceInChallenge::class];
|
||||
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
|
||||
}
|
||||
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->where('round', '=', $round);
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Guess::class, true, $withRelations);
|
||||
}
|
||||
|
||||
public function getAllByPlace(Place $place): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']);
|
||||
$select->where('place_id', '=', $place->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Guess::class);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class MapRepository
|
||||
@@ -16,4 +19,20 @@ class MapRepository
|
||||
{
|
||||
return $this->pdm->selectFromDbById($mapId, Map::class);
|
||||
}
|
||||
|
||||
public function getByPlace(Place $place): ?Map
|
||||
{
|
||||
return $this->getById($place->getMapId());
|
||||
}
|
||||
|
||||
public function getByChallenge(Challenge $challenge): ?Map
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('places', ['maps', 'id'], '=', ['places', 'map_id']);
|
||||
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->limit(1);
|
||||
|
||||
return $this->pdm->selectFromDb($select, Map::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\MultiRoom;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class MultiRoomRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getById(int $id): ?MultiRoom
|
||||
{
|
||||
return $this->pdm->selectFromDbById($id, MultiRoom::class);
|
||||
}
|
||||
|
||||
public function getByRoomId(string $roomId): ?MultiRoom
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('room_id', '=', $roomId);
|
||||
|
||||
return $this->pdm->selectFromDb($select, MultiRoom::class);
|
||||
}
|
||||
|
||||
public function getAllExpired(): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, MultiRoom::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\PlaceInChallenge;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class PlaceInChallengeRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getAllByPlace(Place $place, array $withRelations = []) : Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('place_id', '=', $place->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, PlaceInChallenge::class, true, $withRelations);
|
||||
}
|
||||
|
||||
public function getAllByChallenge(Challenge $challenge) : Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, PlaceInChallenge::class);
|
||||
}
|
||||
|
||||
public function getByPlaceAndChallenge(Place $place, Challenge $challenge) : ?PlaceInChallenge
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('place_id', '=', $place->getId());
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
return $this->pdm->selectFromDb($select, PlaceInChallenge::class);
|
||||
}
|
||||
|
||||
public function getByRoundInChallenge(int $round, Challenge $challenge, array $withRelations = []): ?PlaceInChallenge
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->orderBy('round');
|
||||
$select->limit(1, $round);
|
||||
|
||||
return $this->pdm->selectFromDb($select, PlaceInChallenge::class, true, $withRelations);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
@@ -28,36 +29,174 @@ class PlaceRepository
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
|
||||
}
|
||||
|
||||
//TODO: use Map instead of id
|
||||
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
|
||||
//TODO: use Map and User instead of id
|
||||
public function getRandomNPlaces(int $mapId, int $n, ?int $userId): array
|
||||
{
|
||||
$placesWithoutPano = [];
|
||||
if (!isset($userId)) { // anonymous single player
|
||||
return $this->getRandomNForMapWithValidPano($mapId, $n);
|
||||
} else { // authorized user or multiplayer game with selection based on what the host played before
|
||||
$unvisitedPlaces = $this->getRandomUnvisitedNForMapWithValidPano($mapId, $n, $userId);
|
||||
if (count($unvisitedPlaces) == $n) {
|
||||
return $unvisitedPlaces;
|
||||
}
|
||||
|
||||
$oldPlaces = $this->getRandomOldNForMapWithValidPano($mapId, $n - count($unvisitedPlaces), $userId);
|
||||
|
||||
return array_merge($unvisitedPlaces, $oldPlaces);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: use Map instead of id
|
||||
private function getRandomNForMapWithValidPano(int $mapId, int $n): array
|
||||
{
|
||||
$places = [];
|
||||
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->where('map_id', '=', $mapId);
|
||||
$numberOfPlaces = $select->count();
|
||||
|
||||
$exclude = [];
|
||||
for ($i = 1; $i <= $n; ++$i) {
|
||||
$place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude);
|
||||
|
||||
$places[] = $place;
|
||||
$exclude[] = $place->getId();
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
private function getRandomForMapWithValidPano(int $numberOfPlaces, Select $select, array &$exclude, ?callable $pickRandomInt = null): ?Place
|
||||
{
|
||||
do {
|
||||
$place = $this->selectRandomFromDbForMap($mapId, $exclude);
|
||||
$numberOfPlacesLeft = $numberOfPlaces - count($exclude);
|
||||
$place = $this->selectRandomFromDbForMap($numberOfPlacesLeft, $select, $exclude, $pickRandomInt);
|
||||
if ($place === null) {
|
||||
// there is no more never visited place left
|
||||
return null;
|
||||
}
|
||||
|
||||
$panoId = $place->getFreshPanoId();
|
||||
|
||||
if ($panoId === null) {
|
||||
$placesWithoutPano[] = $exclude[] = $place->getId();
|
||||
$exclude[] = $place->getId();
|
||||
}
|
||||
} while ($panoId === null);
|
||||
|
||||
return $place;
|
||||
}
|
||||
|
||||
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place
|
||||
private function selectRandomFromDbForMap(int $numberOfPlacesLeft, Select $select, array $exclude, ?callable $pickRandomInt): ?Place
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
if ($numberOfPlacesLeft <= 0)
|
||||
return null;
|
||||
|
||||
if (!isset($pickRandomInt)) {
|
||||
$randomOffset = random_int(0, $numberOfPlacesLeft - 1);
|
||||
} else {
|
||||
$randomOffset = $pickRandomInt($numberOfPlacesLeft);
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
return $this->pdm->selectFromDb($select, Place::class);
|
||||
}
|
||||
|
||||
// Never visited places
|
||||
private function getRandomUnvisitedNForMapWithValidPano(int $mapId, int $n, int $userId): array
|
||||
{
|
||||
$places = [];
|
||||
$exclude = [];
|
||||
|
||||
// list of places visited by user
|
||||
$selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place');
|
||||
$selectPlacesByCurrentUser->columns(['place_id', 'last_time']);
|
||||
$selectPlacesByCurrentUser->where('user_id', '=', $userId);
|
||||
$selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user');
|
||||
|
||||
// count the places never visited
|
||||
$selectUnvisited = new Select(\Container::$dbConnection, 'places');
|
||||
$selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
|
||||
$selectUnvisited->where('map_id', '=', $mapId);
|
||||
$selectUnvisited->where('last_time', '=', null);
|
||||
$numberOfUnvisitedPlaces = $selectUnvisited->count();
|
||||
|
||||
// look for as many new places as possible but maximum $n
|
||||
do {
|
||||
$place = $this->getRandomForMapWithValidPano($numberOfUnvisitedPlaces, $selectUnvisited, $exclude);
|
||||
if (isset($place)) {
|
||||
$places[] = $place;
|
||||
$exclude[] = $place->getId();
|
||||
}
|
||||
} while (count($places) < $n && isset($place));
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
// Places visited in the longest time
|
||||
private function getRandomOldNForMapWithValidPano(int $mapId, int $n, int $userId): array
|
||||
{
|
||||
$places = [];
|
||||
$exclude = [];
|
||||
|
||||
// list of places visited by user
|
||||
$selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place');
|
||||
$selectPlacesByCurrentUser->columns(['place_id', 'last_time']);
|
||||
$selectPlacesByCurrentUser->where('user_id', '=', $userId);
|
||||
$selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user');
|
||||
|
||||
// count places that were visited at least once
|
||||
$selectOldPlaces = new Select(\Container::$dbConnection, 'places');
|
||||
$selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
|
||||
$selectOldPlaces->where('map_id', '=', $mapId);
|
||||
$numberOfOldPlaces = $selectOldPlaces->count();
|
||||
|
||||
// set order by datetime, oldest first
|
||||
$selectOldPlaces->orderBy('last_time');
|
||||
|
||||
// selection algorithm with preference (weighting) for older places using Box-Muller transform
|
||||
$pickGaussianRandomInt = function($numberOfPlaces) {
|
||||
$stdev = 0.2;
|
||||
$avg = 0.0;
|
||||
$x = mt_rand() / mt_getrandmax();
|
||||
$y = mt_rand() / mt_getrandmax();
|
||||
$randomNum = abs(sqrt(-2 * log($x)) * cos(2 * pi() * $y) * $stdev + $avg);
|
||||
return (int) min($randomNum * $numberOfPlaces, $numberOfPlaces - 1);
|
||||
};
|
||||
|
||||
// look for n - numberOfUnvisitedPlaces places
|
||||
while (count($places) < $n)
|
||||
{
|
||||
$place = $this->getRandomForMapWithValidPano($numberOfOldPlaces, $selectOldPlaces, $exclude, $pickGaussianRandomInt);
|
||||
if (isset($place))
|
||||
{
|
||||
$places[] = $place;
|
||||
$exclude[] = $place->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
public function getByRoundInChallenge(Challenge $challenge, int $round): ?Place
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->orderBy('round');
|
||||
$select->limit(1, $round);
|
||||
|
||||
return $this->pdm->selectFromDb($select, Place::class);
|
||||
}
|
||||
|
||||
public function getAllInChallenge(Challenge $challenge): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
$select->orderBy('round');
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\UserInChallenge;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class UserInChallengeRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getAllByUser(User $user) : Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class);
|
||||
}
|
||||
|
||||
public function getAllByChallenge(Challenge $challenge) : Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class);
|
||||
}
|
||||
|
||||
public function getAllByChallengeWithUsers(Challenge $challenge) : Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class, true, [User::class]);
|
||||
}
|
||||
|
||||
public function getByUserIdAndChallenge(int $userId, Challenge $challenge): ?UserInChallenge
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $userId);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserInChallenge::class);
|
||||
}
|
||||
|
||||
public function getByUserIdAndToken(int $userId, string $token_str, array $withRelations = []): ?UserInChallenge
|
||||
{
|
||||
if (count($withRelations)) {
|
||||
$necessaryRelations = [Challenge::class];
|
||||
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations));
|
||||
}
|
||||
|
||||
// validate token string
|
||||
if (!ctype_xdigit($token_str)) {
|
||||
return null;
|
||||
}
|
||||
// convert token to int
|
||||
$token = hexdec($token_str);
|
||||
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $userId);
|
||||
$select->where('token', '=', $token);
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserInChallenge::class, true, $withRelations);
|
||||
}
|
||||
|
||||
public function isUserParticipatingInChallenge(int $userId, Challenge $challenge): bool
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'user_in_challenge');
|
||||
$select->where('user_id', '=', $userId);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
return $select->count() != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\UserPlayedPlace;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
class UserPlayedPlaceRepository
|
||||
{
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pdm = new PersistentDataManager();
|
||||
}
|
||||
|
||||
public function getByUser(User $user): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class);
|
||||
}
|
||||
|
||||
public function getAllByPlace(Place $place): Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('place_id', '=', $place->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class);
|
||||
}
|
||||
|
||||
public function getAllByUser(User $user) : Generator
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $user->getId());
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class);
|
||||
}
|
||||
|
||||
public function getByUserIdAndPlaceId(int $userId, int $placeId) : ?UserPlayedPlace
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection);
|
||||
$select->where('user_id', '=', $userId);
|
||||
$select->where('place_id', '=', $placeId);
|
||||
|
||||
return $this->pdm->selectFromDb($select, UserPlayedPlace::class);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
use DateTime;
|
||||
use Generator;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\PersistentData\Model\Guess;
|
||||
use MapGuesser\PersistentData\Model\User;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
|
||||
@@ -44,4 +45,9 @@ class UserRepository
|
||||
|
||||
yield from $this->pdm->selectMultipleFromDb($select, User::class);
|
||||
}
|
||||
|
||||
public function getByGuess(Guess $guess): ?User
|
||||
{
|
||||
return $this->getById($guess->getUserId());
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class Request implements IRequest
|
||||
}
|
||||
}
|
||||
|
||||
public function setParsedRouteParams(array &$routeParams)
|
||||
public function setParsedRouteParams(array &$routeParams): void
|
||||
{
|
||||
$this->routeParams = &$routeParams;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ class Session implements ISession
|
||||
$this->data = &$data;
|
||||
}
|
||||
|
||||
public function has($key): bool
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
public function get($key)
|
||||
public function get(string $key)
|
||||
{
|
||||
if (isset($this->data[$key])) {
|
||||
return $this->data[$key];
|
||||
@@ -25,12 +25,12 @@ class Session implements ISession
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set($key, $value): void
|
||||
public function set(string $key, $value): void
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function delete($key): void
|
||||
public function delete(string $key): void
|
||||
{
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class HtmlContent extends ContentBase
|
||||
|
||||
require ROOT . '/cache/views/' . $this->view . '.php';
|
||||
|
||||
// @phpstan-ignore-next-line - SCRIPT_STARTED is defined in main.php
|
||||
echo '<!-- __debug__runtime: ' . round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1) . ' -->';
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ class JsonContent extends ContentBase
|
||||
|
||||
public function render(): void
|
||||
{
|
||||
// @phpstan-ignore-next-line - SCRIPT_STARTED is defined in main.php
|
||||
$this->data['__debug__runtime'] = round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1);
|
||||
|
||||
echo json_encode($this->data);
|
||||
|
||||
@@ -16,7 +16,7 @@ class Redirect implements IRedirect
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
if (preg_match('/^http(s)?/', $this->target)) {
|
||||
if (preg_match('/^http(s)?/', $this->target) === 1) {
|
||||
$link = $this->target;
|
||||
} else {
|
||||
$link = \Container::$request->getBase() . '/' . $this->target;
|
||||
|
||||
@@ -30,7 +30,7 @@ class Route
|
||||
$link = [];
|
||||
|
||||
foreach ($this->pattern as $fragment) {
|
||||
if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches)) {
|
||||
if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches) === 1) {
|
||||
if (isset($parameters[$matches[1]])) {
|
||||
$link[] = $parameters[$matches[1]];
|
||||
unset($parameters[$matches[1]]);
|
||||
@@ -61,7 +61,7 @@ class Route
|
||||
$parameters = [];
|
||||
|
||||
foreach ($path as $i => $fragment) {
|
||||
if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches)) {
|
||||
if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches) === 1) {
|
||||
$parameters[$matches[1]] = $fragment;
|
||||
} elseif ($fragment != $this->pattern[$i]) {
|
||||
return null;
|
||||
|
||||
@@ -76,7 +76,7 @@ class RouteCollection
|
||||
|
||||
$this->searchTable[$method][$groupNumber][] = $route;
|
||||
|
||||
while (preg_match('/^{\\w+\\?}$/', end($pattern))) {
|
||||
while (preg_match('/^{\\w+\\?}$/', end($pattern)) === 1) {
|
||||
$groupNumber--;
|
||||
array_pop($pattern);
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class DatabaseSessionHandler implements ISessionHandler
|
||||
return true;
|
||||
}
|
||||
|
||||
public function gc($maxlifetime): int
|
||||
public function gc($maxlifetime): bool
|
||||
{
|
||||
// empty on purpose
|
||||
// old sessions are deleted by MaintainDatabaseCommand
|
||||
@@ -84,7 +84,7 @@ class DatabaseSessionHandler implements ISessionHandler
|
||||
|
||||
public function validateId($id): bool
|
||||
{
|
||||
return preg_match('/^[a-f0-9]{32}$/', $id);
|
||||
return preg_match('/^[a-f0-9]{32}$/', $id) === 1;
|
||||
}
|
||||
|
||||
public function updateTimestamp($id, $data): bool
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php namespace MapGuesser\Util;
|
||||
|
||||
use MapGuesser\Http\Request;
|
||||
|
||||
class CaptchaValidator
|
||||
{
|
||||
public function validate(string $response)
|
||||
{
|
||||
$request = new Request('https://www.google.com/recaptcha/api/siteverify', Request::HTTP_GET);
|
||||
$request->setQuery([
|
||||
'secret' => $_ENV['RECAPTCHA_SECRET'],
|
||||
'response' => $response
|
||||
]);
|
||||
|
||||
$response = $request->send();
|
||||
|
||||
return json_decode($response->getBody(), true);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class Bounds
|
||||
|
||||
public static function createDirectly(float $southLat, float $westLng, float $northLat, float $eastLng): Bounds
|
||||
{
|
||||
$instance = new static();
|
||||
$instance = new self();
|
||||
|
||||
$instance->southLat = $southLat;
|
||||
$instance->westLng = $westLng;
|
||||
|
||||
@@ -11,7 +11,7 @@ class JwtParser
|
||||
}
|
||||
}
|
||||
|
||||
public function setToken(string $token)
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token));
|
||||
}
|
||||
|
||||
+3
-3
@@ -65,7 +65,7 @@ class Linker
|
||||
while (($line = fgets($inputFileHandle)) !== false) {
|
||||
++$lineNumber;
|
||||
|
||||
if (preg_match('/^\s*@yields\(\'([\w\/]+)\'\)\s*$/', $line, $matches)) {
|
||||
if (preg_match('/^\s*@yields\(\'([\w\/]+)\'\)\s*$/', $line, $matches) === 1) {
|
||||
if (isset($sections[$matches[1]])) {
|
||||
fwrite($outputFileHandle, $sections[$matches[1]]);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class Linker
|
||||
fclose($outputFileHandle);
|
||||
}
|
||||
|
||||
private function generateAssets(ParsedFragment $fragment, array &$sections)
|
||||
private function generateAssets(ParsedFragment $fragment, array &$sections): void
|
||||
{
|
||||
foreach ($fragment->getCss() as $cssFile) {
|
||||
$asset = $this->parseAsset($cssFile);
|
||||
@@ -134,7 +134,7 @@ class Linker
|
||||
{
|
||||
$output = [];
|
||||
|
||||
if (preg_match('/^[\w\/\.]+$/', $asset)) {
|
||||
if (preg_match('/^[\w\/\.]+$/', $asset) === 1) {
|
||||
if (
|
||||
empty($_ENV['DEV']) &&
|
||||
filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT
|
||||
|
||||
+7
-7
@@ -108,7 +108,7 @@ class Parser
|
||||
|
||||
private function matchCss(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@css\((.*)\)\s*$/', $line, $matches)) {
|
||||
if (preg_match('/^\s*@css\((.*)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ class Parser
|
||||
|
||||
private function matchJs(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@js\((.*)\)\s*$/', $line, $matches)) {
|
||||
if (preg_match('/^\s*@js\((.*)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class Parser
|
||||
|
||||
private function matchExtends(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches)) {
|
||||
if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class Parser
|
||||
|
||||
private function matchSection(string $line): ?string
|
||||
{
|
||||
if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches)) {
|
||||
if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
@@ -144,16 +144,16 @@ class Parser
|
||||
|
||||
private function matchEndSection(string $line): bool
|
||||
{
|
||||
return preg_match('/^\s*@endsection(?:\(\))?\s*$/', $line);
|
||||
return preg_match('/^\s*@endsection(?:\(\))?\s*$/', $line) === 1;
|
||||
}
|
||||
|
||||
private function matchExtra(string $line): bool
|
||||
{
|
||||
return preg_match('/^\s*@extra(?:\(\))?\s*$/', $line);
|
||||
return preg_match('/^\s*@extra(?:\(\))?\s*$/', $line) === 1;
|
||||
}
|
||||
|
||||
private function matchEndExtra(string $line): bool
|
||||
{
|
||||
return preg_match('/^\s*@endextra(?:\(\))?\s*$/', $line);
|
||||
return preg_match('/^\s*@endextra(?:\(\))?\s*$/', $line) === 1;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
use MapGuesser\PersistentData\Model\Model;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class OtherModel
|
||||
{
|
||||
}
|
||||
|
||||
class DummyModel extends Model
|
||||
{
|
||||
protected static string $table = 'test_table';
|
||||
|
||||
@@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ParserTest extends TestCase
|
||||
{
|
||||
const TEST_VIEWS_PATH = __DIR__ . '/../assets/views';
|
||||
const TEST_VIEWS_PATH = __DIR__ . '/../../views/tests';
|
||||
|
||||
public function testCanParseViewWithoutExtends(): void
|
||||
{
|
||||
|
||||
@@ -9,24 +9,24 @@
|
||||
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
|
||||
<div class="inputWithButton">
|
||||
<input type="password" name="password" placeholder="Current password" required minlength="6" autofocus><!--
|
||||
<input type="password" class="text name="password" placeholder="Current password" required minlength="6" autofocus><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
|
||||
</div>
|
||||
<?php elseif ($user['password'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with your password to modify your account.</p>
|
||||
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
|
||||
<input type="password" class="text big fullWidth" name="password" placeholder="Current password" required minlength="6" autofocus>
|
||||
<?php elseif ($user['google_sub'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with Google to modify your account.</p>
|
||||
<div class="inputWithButton">
|
||||
<input type="text" name="password" placeholder="Authenticate with Google..." disabled><!--
|
||||
<input type="text" class="text" name="password" placeholder="Authenticate with Google..." disabled><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?>
|
||||
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>
|
||||
<input class="big fullWidth marginTop" type="password" name="password_new" placeholder="New password" minlength="6">
|
||||
<input class="big fullWidth marginTop" type="password" name="password_new_confirm" placeholder="New password confirmation" minlength="6">
|
||||
<input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= $user['email'] ?>" disabled>
|
||||
<input type="password" class="text big fullWidth marginTop" name="password_new" placeholder="New password" minlength="6">
|
||||
<input type="password" class="text big fullWidth marginTop" name="password_new_confirm" placeholder="New password confirmation" minlength="6">
|
||||
<p id="accountFormError" class="formError justify marginTop"></p>
|
||||
<div class="right marginTop">
|
||||
<button type="submit" name="submit" disabled>Save</button>
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with your password or with Google to delete your account.</p>
|
||||
<div class="inputWithButton">
|
||||
<input type="password" name="password" placeholder="Current password" required minlength="6" autofocus><!--
|
||||
<input type="password" class="text" name="password" placeholder="Current password" required minlength="6" autofocus><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
|
||||
</div>
|
||||
<?php elseif ($user['password'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with your password to delete your account.</p>
|
||||
<input class="big fullWidth" type="password" name="password" placeholder="Current password" required minlength="6" autofocus>
|
||||
<input type="password" class="text big fullWidth" name="password" placeholder="Current password" required minlength="6" autofocus>
|
||||
<?php elseif ($user['google_sub'] !== null): ?>
|
||||
<p class="justify small">Please confirm your identity with Google to delete your account.</p>
|
||||
<div class="inputWithButton">
|
||||
<input type="text" name="password" placeholder="Authenticate with Google..." disabled><!--
|
||||
<input type="text" class="text" name="password" placeholder="Authenticate with Google..." disabled><!--
|
||||
--><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -6,12 +6,22 @@
|
||||
@js(node_modules/leaflet/dist/leaflet.js)
|
||||
@js(node_modules/leaflet.markercluster/dist/leaflet.markercluster.js)
|
||||
@js(https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>)
|
||||
@js(https://unpkg.com/@googlemaps/markerclustererplus/dist/index.min.js)
|
||||
@js(js/map_editor.js)
|
||||
|
||||
@extends(templates/layout_full)
|
||||
|
||||
@section(subheader)
|
||||
<span><a href="javascript:;" id="mapName" title="Edit map data"><?= $mapName ?></a></span><!--
|
||||
--><span class="inline hideOnMobile" id="mapSelection">
|
||||
<button id="mapSelector">
|
||||
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/map.svg?rev=<?= REVISION ?>" alt="Map Selector" />
|
||||
</button>
|
||||
</span><!--
|
||||
--><span class="inline hideOnMobile">
|
||||
<input type="text" class="text" id="jumpCoordinates" placeholder="Insert coordinates here" />
|
||||
<button id="jumpButton" disabled >Jump</button>
|
||||
</span><!--
|
||||
--><span><!--
|
||||
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
|
||||
--><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -42,8 +52,11 @@
|
||||
<div id="metadata" class="modal">
|
||||
<h2>Edit map data</h2>
|
||||
<form id="metadataForm" class="marginTop" data-no-submit="true">
|
||||
<input class="fullWidth" type="text" name="name" value="<?= $mapName ?>" placeholder="Name of the map">
|
||||
<textarea class="fullWidth marginTop" name="description" rows="4" placeholder="Description of the map"><?= $mapDescription ?></textarea>
|
||||
<input type="text" class="text fullWidth" name="name" value="<?= $mapName ?>" placeholder="Name of the map">
|
||||
<textarea class="fullWidth marginTop" name="description" rows="4" placeholder="Description of the map"><?= $mapDescription ?></textarea>
|
||||
<div class="marginTop">
|
||||
<input type="checkbox" id="unlisted" name="unlisted" <?= $mapUnlisted ? 'checked' : '' ?>><label for="unlisted">Unlisted</label>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button class="marginTop marginRight" type="submit">Apply</button><!--
|
||||
--><button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
|
||||
@@ -53,7 +66,20 @@
|
||||
@endsection
|
||||
|
||||
@section(main)
|
||||
<div id="map"></div>
|
||||
<div id="map" class="map">
|
||||
<div id="lmap" class="map"></div>
|
||||
<div id="gmap" class="map"></div>
|
||||
<div id="overlayControl" class="circleControl">
|
||||
<div id="streetViewCoverSelector" class="controlItem" title="Toggle Street View Cover">
|
||||
<div>
|
||||
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/circle_background.svg?rev=<?= REVISION ?>" alt="Circle Background" class="controlBackground" />
|
||||
</div>
|
||||
<div>
|
||||
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/street-view-cover.svg?rev=<?= REVISION ?>" alt="Street View Cover" class="controlIcon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="panorama"></div>
|
||||
<div id="noPano">
|
||||
<p class="bold">No panorama is available for this location.</p>
|
||||
@@ -71,6 +97,7 @@
|
||||
@section(pageScript)
|
||||
<script>
|
||||
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
|
||||
var tileSubdomains = '<?= $_ENV['LEAFLET_TILESERVER_SUBDOMAINS'] ?>';
|
||||
var tileAttribution = '<?= $_ENV['LEAFLET_TILESERVER_ATTRIBUTION'] ?>';
|
||||
var mapId = <?= $mapId ?>;
|
||||
var mapBounds = <?= json_encode($bounds) ?>;
|
||||
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user