Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94624d5b2c | ||
|
|
71c728d56c | ||
|
|
216d30329f | ||
|
|
3f8311d708 | ||
|
|
7f8c1eb291 | ||
|
|
d57a4c63e0 | ||
|
|
ed8e1773c4 | ||
|
|
2963bfe739 | ||
|
|
7232b322ab | ||
|
|
e6819b6dd3 | ||
|
|
01a4fdf5e5 | ||
|
|
1cc3294ee7 | ||
|
|
ad0b8d078f | ||
|
|
899817a853 | ||
|
|
c626e36bbb | ||
|
|
8d8074977b | ||
|
|
8b3c95bdc7 | ||
|
|
b2535ad78a | ||
|
|
886bd02f88 | ||
|
|
7143c7ec63 | ||
|
|
fb7c0e7a5c | ||
|
|
8136273b33 | ||
|
|
3c6a7a3c5f | ||
|
|
01f3c7c8cf | ||
|
|
66c871fbc2 | ||
|
|
807b4d024f | ||
|
|
430b32d0c6 | ||
|
|
11eee64895
|
||
|
|
cc24f96395 | ||
|
|
c3e5a6db1c | ||
|
|
398204c5d5 | ||
|
|
aaa5a1daba | ||
|
|
c112a25409
|
||
|
|
35cab17c21
|
||
|
|
f9c1829a00
|
||
|
|
e03850ce05
|
||
|
|
55ec226407 | ||
|
|
6ef04fcd4e
|
||
|
|
3045d8acbb
|
||
|
|
19a1b79b0a | ||
|
|
d432a5b584 | ||
|
|
8b9345eb36 | ||
|
|
e1eb0077b1 | ||
|
|
745bda11c0 | ||
|
|
0bb31cbe14 | ||
|
|
ef8ad69506 | ||
|
|
f10ad37a74 | ||
|
|
70b60d2fb8
|
||
|
|
7e68580db6
|
||
|
|
8218883faf | ||
|
|
145cb5f637 | ||
|
|
6d3f74f911 | ||
|
|
3d663fb5d6 | ||
|
|
759f654a0d | ||
|
|
c09cce3c97 | ||
|
|
276e331a47
|
||
|
|
62f4fd7b84 | ||
|
|
b22f13887f | ||
|
|
2a3f2212a7 | ||
|
|
98df08e5fa | ||
|
|
8f0cfd4489 | ||
|
|
d42280eb10 | ||
|
|
0739c65961 | ||
|
|
3122696364 | ||
|
|
462f6cde80 | ||
|
|
3f4fa83d87 | ||
|
|
ea599809b8 | ||
|
|
f0b0de405c | ||
|
|
ffc4aaf110 | ||
|
|
58cdc1be8d | ||
|
|
4039202268
|
||
|
|
4677cc5229 | ||
|
|
8800724aa9
|
||
|
|
08c38c6374 | ||
|
|
854775011c | ||
|
|
e33d8c02e9 | ||
|
|
3d52c967b6 | ||
|
|
5c8f3d6fb9 | ||
|
|
b9927f79fc | ||
|
|
56f3a9b380 | ||
|
|
a0d41c388b | ||
|
|
092cc63148 | ||
|
|
95553296bd | ||
|
|
9330227879 | ||
|
|
faaff33841 | ||
|
|
508a6899d8 | ||
|
|
be0f7b64b8 | ||
|
|
15e3248ee1 | ||
|
|
04a897c958 | ||
|
|
845d77ebda | ||
|
|
8bc3fda49b | ||
|
|
d78817180b | ||
|
|
0f810bd2ba | ||
|
|
9d1fc8b60c
|
||
|
|
dd46fb3d32
|
||
|
|
1a40e3a18d | ||
|
|
1aa5d3b43e
|
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_SERVER_API_KEY=your_google_maps_server_api_key
|
||||||
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
|
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
|
||||||
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
|
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
|
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
|
||||||
STATIC_ROOT=/static
|
STATIC_ROOT=/static
|
||||||
MAIL_FROM=mapguesser@mapguesser-dev.ch
|
MAIL_FROM=mapguesser@mapguesser-dev.ch
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ git clone https://gitea.e5tv.hu/esoko/mapguesser.git
|
|||||||
|
|
||||||
All the commands listed here should be executed from the repository root.
|
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`.
|
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 +24,17 @@ All you have to do is executing the following command:
|
|||||||
docker-compose up -d
|
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
|
### Initialize project
|
||||||
|
|
||||||
@@ -40,15 +48,33 @@ 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.
|
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
|
||||||
|
```
|
||||||
|
|
||||||
### (Production only) Create cron job
|
### (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
|
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
|
||||||
@@ -56,13 +82,19 @@ To maintain database (delete inactive users, old sessions etc.), the command `db
|
|||||||
|
|
||||||
### Finalize installation
|
### 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
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
+2
-2
@@ -5,7 +5,7 @@ process.title = 'mapguesser-multi';
|
|||||||
class MultiGame {
|
class MultiGame {
|
||||||
static ROUND_TIMEOUT_DEFAULT = 120000;
|
static ROUND_TIMEOUT_DEFAULT = 120000;
|
||||||
static ROUND_TIMEOUT_MINIMUM = 15000;
|
static ROUND_TIMEOUT_MINIMUM = 15000;
|
||||||
static ROUND_TIMEOUT_DIVIDER = 2;
|
static ROUND_TIMEOUT_DIVIDER = 1.5;
|
||||||
static ROUND_TIMEOUT_OFFSET = 500;
|
static ROUND_TIMEOUT_OFFSET = 500;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -155,7 +155,7 @@ class MultiGame {
|
|||||||
} else {
|
} else {
|
||||||
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
|
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
|
||||||
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
||||||
round.timeout = round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER;
|
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
|
||||||
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
|
||||||
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
|
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -8,6 +8,6 @@
|
|||||||
"ws": "^7.4.4"
|
"ws": "^7.4.4"
|
||||||
},
|
},
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"author": "Pőcze Bence <bence@pocze.ch>",
|
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
|
||||||
"license": "GNU AGPL 3.0"
|
"license": "GNU AGPL 3.0"
|
||||||
}
|
}
|
||||||
@@ -149,13 +149,17 @@
|
|||||||
color: #701919;
|
color: #701919;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#navigation {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 599px) {
|
@media screen and (max-width: 599px) {
|
||||||
#mapName {
|
#mapName {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#showGuessButtonContainer {
|
#showGuessButtonContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 65px;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -172,6 +176,10 @@
|
|||||||
#scoreBarBase {
|
#scoreBarBase {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
#navigation {
|
||||||
|
bottom: 25px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 600px) {
|
@media screen and (min-width: 600px) {
|
||||||
@@ -215,6 +223,10 @@
|
|||||||
#scoreBarBase {
|
#scoreBarBase {
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
#navigation {
|
||||||
|
bottom: 50px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
@media screen and (max-height: 424px) {
|
@media screen and (max-height: 424px) {
|
||||||
#guess {
|
#guess {
|
||||||
top: 10px;
|
top: 10px;
|
||||||
@@ -229,5 +241,9 @@
|
|||||||
right: 20px;
|
right: 20px;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
}
|
}
|
||||||
|
#navigation {
|
||||||
|
bottom: 30px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#map {
|
.map {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -7,6 +7,13 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mapSelection img {
|
||||||
|
display: inline;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
/* modify the cursor for the Leaflet map */
|
/* modify the cursor for the Leaflet map */
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
@@ -48,6 +55,12 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#overlayControl {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
#deleteButton {
|
#deleteButton {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -65,6 +78,9 @@
|
|||||||
#placeControl {
|
#placeControl {
|
||||||
top: calc(50% + 10px);
|
top: calc(50% + 10px);
|
||||||
}
|
}
|
||||||
|
.hideOnMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1000px), (max-height: 599px) {
|
@media screen and (min-width: 1000px), (max-height: 599px) {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ html, body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
button::-moz-focus-inner, input::-moz-focus-inner {
|
button::-moz-focus-inner, input::-moz-focus-inner {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -374,6 +378,7 @@ header>p>span:not(:last-child) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
background-color: #ffffff;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +443,43 @@ div.box {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.circleControl {
|
||||||
|
position: absolute;
|
||||||
|
width: 60px;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circleControl .controlItem {
|
||||||
|
height: 60px;
|
||||||
|
margin-top: 10px;
|
||||||
|
opacity: 70%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circleControl .controlItem:hover {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circleControl .controlItem div {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circleControl .controlBackground {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circleControl .controlIcon {
|
||||||
|
width: 75%;
|
||||||
|
height: 75%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
-ms-transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 599px) {
|
@media screen and (max-width: 599px) {
|
||||||
header h1 span {
|
header h1 span {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -464,6 +506,12 @@ div.box {
|
|||||||
div.box {
|
div.box {
|
||||||
width: initial;
|
width: initial;
|
||||||
}
|
}
|
||||||
|
.circleControl {
|
||||||
|
width: 45px;
|
||||||
|
}
|
||||||
|
.circleControl .controlItem {
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 600px) {
|
@media screen and (min-width: 600px) {
|
||||||
@@ -493,6 +541,12 @@ div.box {
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
.circleControl {
|
||||||
|
width: 45px;
|
||||||
|
}
|
||||||
|
.circleControl .controlItem {
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-height: 400px) and (max-height: 499px) {
|
@media screen and (min-height: 400px) and (max-height: 499px) {
|
||||||
|
|||||||
@@ -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,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 |
@@ -17,7 +17,7 @@
|
|||||||
adaptGuess: false,
|
adaptGuess: false,
|
||||||
googleLink: null,
|
googleLink: null,
|
||||||
|
|
||||||
readyToContinue: false,
|
readyToContinue: true,
|
||||||
timeoutEnd: null,
|
timeoutEnd: null,
|
||||||
countdownHandler: null,
|
countdownHandler: null,
|
||||||
|
|
||||||
@@ -97,6 +97,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (data) {
|
initialize: function (data) {
|
||||||
|
Game.readyToContinue = false;
|
||||||
|
|
||||||
if (data.history.length === 0 && !data.place) {
|
if (data.history.length === 0 && !data.place) {
|
||||||
var div = document.getElementById('players');
|
var div = document.getElementById('players');
|
||||||
|
|
||||||
@@ -334,6 +336,9 @@
|
|||||||
document.getElementById('guess').style.visibility = null;
|
document.getElementById('guess').style.visibility = null;
|
||||||
document.getElementById('guess').classList.remove('result');
|
document.getElementById('guess').classList.remove('result');
|
||||||
|
|
||||||
|
// needs to be set visible after the show guess map hid it in mobile view
|
||||||
|
document.getElementById("navigation").style.visibility = 'visible';
|
||||||
|
|
||||||
Game.initialize();
|
Game.initialize();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -358,7 +363,10 @@
|
|||||||
document.getElementById('panoCover').style.visibility = 'hidden';
|
document.getElementById('panoCover').style.visibility = 'hidden';
|
||||||
document.getElementById('showGuessButton').style.visibility = null;
|
document.getElementById('showGuessButton').style.visibility = null;
|
||||||
document.getElementById('guess').style.visibility = null;
|
document.getElementById('guess').style.visibility = null;
|
||||||
document.getElementById('guess').classList.remove('result')
|
document.getElementById('guess').classList.remove('result');
|
||||||
|
|
||||||
|
// needs to be set visible after the show guess map hid it in mobile view
|
||||||
|
document.getElementById("navigation").style.visibility = 'visible';
|
||||||
|
|
||||||
Game.map.setOptions({
|
Game.map.setOptions({
|
||||||
draggableCursor: 'crosshair'
|
draggableCursor: 'crosshair'
|
||||||
@@ -379,6 +387,10 @@
|
|||||||
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
||||||
|
|
||||||
Game.loadPano(Game.panoId, Game.pov);
|
Game.loadPano(Game.panoId, Game.pov);
|
||||||
|
|
||||||
|
// update the compass
|
||||||
|
const heading = Game.panorama.getPov().heading;
|
||||||
|
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
|
||||||
},
|
},
|
||||||
|
|
||||||
handleErrorResponse: function (error) {
|
handleErrorResponse: function (error) {
|
||||||
@@ -398,6 +410,10 @@
|
|||||||
MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
|
MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'game_not_found':
|
||||||
|
MapGuesser.showModalWithContent('Error', 'The game room was not found by this ID. Please check the link.');
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
|
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
|
||||||
break
|
break
|
||||||
@@ -855,6 +871,9 @@
|
|||||||
|
|
||||||
Game.panorama.addListener('pov_changed', function () {
|
Game.panorama.addListener('pov_changed', function () {
|
||||||
Game.rewriteGoogleLink();
|
Game.rewriteGoogleLink();
|
||||||
|
|
||||||
|
const heading = Game.panorama.getPov().heading;
|
||||||
|
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
|
||||||
});
|
});
|
||||||
|
|
||||||
if (COOKIES_CONSENT) {
|
if (COOKIES_CONSENT) {
|
||||||
@@ -864,11 +883,13 @@
|
|||||||
document.getElementById('showGuessButton').onclick = function () {
|
document.getElementById('showGuessButton').onclick = function () {
|
||||||
this.style.visibility = 'hidden';
|
this.style.visibility = 'hidden';
|
||||||
document.getElementById('guess').style.visibility = 'visible';
|
document.getElementById('guess').style.visibility = 'visible';
|
||||||
|
document.getElementById('navigation').style.visibility = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('closeGuessButton').onclick = function () {
|
document.getElementById('closeGuessButton').onclick = function () {
|
||||||
document.getElementById('showGuessButton').style.visibility = null;
|
document.getElementById('showGuessButton').style.visibility = null;
|
||||||
document.getElementById('guess').style.visibility = null;
|
document.getElementById('guess').style.visibility = null;
|
||||||
|
document.getElementById('navigation').style.visibility = 'visible';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('guessButton').onclick = function () {
|
document.getElementById('guessButton').onclick = function () {
|
||||||
@@ -936,4 +957,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('returnToStart').onclick = function () {
|
||||||
|
Game.loadPano(Game.panoId, Game.pov);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('compassContainer').onclick = function () {
|
||||||
|
Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch });
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
+345
-49
@@ -7,7 +7,6 @@
|
|||||||
description: null
|
description: null
|
||||||
},
|
},
|
||||||
map: null,
|
map: null,
|
||||||
markers: null,
|
|
||||||
panorama: null,
|
panorama: null,
|
||||||
selectedMarker: null,
|
selectedMarker: null,
|
||||||
added: {},
|
added: {},
|
||||||
@@ -87,7 +86,7 @@
|
|||||||
sv.getPanorama({
|
sv.getPanorama({
|
||||||
location: location,
|
location: location,
|
||||||
preference: google.maps.StreetViewPreference.NEAREST,
|
preference: google.maps.StreetViewPreference.NEAREST,
|
||||||
radius: 100,
|
radius: MapEditor.map.getSearchRadius(location),
|
||||||
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
|
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
|
||||||
}, function (data, status) {
|
}, function (data, status) {
|
||||||
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
|
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
|
||||||
@@ -118,16 +117,13 @@
|
|||||||
MapEditor.resetSelected();
|
MapEditor.resetSelected();
|
||||||
MapEditor.selectedMarker = marker;
|
MapEditor.selectedMarker = marker;
|
||||||
|
|
||||||
MapEditor.map.invalidateSize(true);
|
MapEditor.map.resize();
|
||||||
MapEditor.map.panTo(marker.getLatLng());
|
MapEditor.map.panTo(marker.getLatLng());
|
||||||
|
|
||||||
MapEditor.panorama.setVisible(false);
|
MapEditor.panorama.setVisible(false);
|
||||||
|
|
||||||
if (marker.placeId) {
|
if (marker.placeId) {
|
||||||
MapEditor.markers.removeLayer(MapEditor.selectedMarker);
|
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
|
||||||
MapEditor.map.addLayer(MapEditor.selectedMarker);
|
|
||||||
marker.setIcon(IconCollection.iconBlue);
|
|
||||||
marker.setZIndexOffset(2000);
|
|
||||||
|
|
||||||
document.getElementById('deleteButton').style.display = 'block';
|
document.getElementById('deleteButton').style.display = 'block';
|
||||||
|
|
||||||
@@ -165,13 +161,13 @@
|
|||||||
var placeId = MapEditor.selectedMarker.placeId
|
var placeId = MapEditor.selectedMarker.placeId
|
||||||
|
|
||||||
if (places[placeId].id && !del) {
|
if (places[placeId].id && !del) {
|
||||||
MapEditor.map.removeLayer(MapEditor.selectedMarker);
|
MapEditor.map.changeMarkerIcon(
|
||||||
MapEditor.markers.addLayer(MapEditor.selectedMarker);
|
MapEditor.selectedMarker,
|
||||||
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
|
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
|
||||||
MapEditor.selectedMarker.setZIndexOffset(1000);
|
);
|
||||||
} else {
|
} else {
|
||||||
delete places[placeId];
|
delete places[placeId];
|
||||||
MapEditor.map.removeLayer(MapEditor.selectedMarker);
|
MapEditor.map.removeMarker(MapEditor.selectedMarker);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('deleteButton').style.display = 'none';
|
document.getElementById('deleteButton').style.display = 'none';
|
||||||
@@ -223,7 +219,7 @@
|
|||||||
MapEditor.resetSelected(del);
|
MapEditor.resetSelected(del);
|
||||||
MapEditor.selectedMarker = null;
|
MapEditor.selectedMarker = null;
|
||||||
|
|
||||||
MapEditor.map.invalidateSize(true);
|
MapEditor.map.resize();
|
||||||
},
|
},
|
||||||
|
|
||||||
deletePlace: function () {
|
deletePlace: function () {
|
||||||
@@ -239,6 +235,7 @@
|
|||||||
|
|
||||||
delete MapEditor.added[placeId];
|
delete MapEditor.added[placeId];
|
||||||
delete MapEditor.edited[placeId];
|
delete MapEditor.edited[placeId];
|
||||||
|
delete places[placeId];
|
||||||
|
|
||||||
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
|
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
|
||||||
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
|
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
|
||||||
@@ -313,7 +310,49 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var IconCollection = {
|
var Util = {
|
||||||
|
getHighResData: function () {
|
||||||
|
if (window.devicePixelRatio >= 4) {
|
||||||
|
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
|
||||||
|
} else if (window.devicePixelRatio >= 2) {
|
||||||
|
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
|
||||||
|
} else {
|
||||||
|
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
extractCoordinates: function (coordinatesStr) {
|
||||||
|
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
|
||||||
|
var delimiters = [',', ' ', ';'];
|
||||||
|
|
||||||
|
coordinatesStr = coordinatesStr.trim();
|
||||||
|
if (coordinatesStr.length == 0) {
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var delimiter of delimiters) {
|
||||||
|
if (coordinatesStr.indexOf(delimiter) != -1) {
|
||||||
|
|
||||||
|
var coordinatesArr = coordinatesStr.split(delimiter);
|
||||||
|
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
|
||||||
|
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
|
||||||
|
|
||||||
|
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
|
||||||
|
coordinates.valid = true;
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var LMapWrapper = {
|
||||||
|
map: null,
|
||||||
|
markers: null,
|
||||||
|
divId: null,
|
||||||
|
iconCollection: {
|
||||||
iconGreen: L.icon({
|
iconGreen: L.icon({
|
||||||
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
|
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
|
||||||
iconSize: [24, 32],
|
iconSize: [24, 32],
|
||||||
@@ -328,55 +367,57 @@
|
|||||||
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
|
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
|
||||||
iconSize: [24, 32],
|
iconSize: [24, 32],
|
||||||
iconAnchor: [12, 32]
|
iconAnchor: [12, 32]
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
var Util = {
|
|
||||||
getHighResData: function () {
|
|
||||||
if (window.devicePixelRatio >= 4) {
|
|
||||||
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
|
|
||||||
} else if (window.devicePixelRatio >= 2) {
|
|
||||||
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
|
|
||||||
} else {
|
|
||||||
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
MapEditor.map = L.map('map', {
|
|
||||||
zoomControl: false
|
|
||||||
});
|
|
||||||
|
|
||||||
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";
|
||||||
|
|
||||||
|
if (!LMapWrapper.map) {
|
||||||
|
LMapWrapper.divId = divId;
|
||||||
|
LMapWrapper.map = L.map(LMapWrapper.divId, {
|
||||||
|
center: { lat: 0., lng: 0. },
|
||||||
|
zoom: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
MapEditor.select(marker);
|
LMapWrapper.map.on('click', function (e) {
|
||||||
|
LMapWrapper.placeMarker(e.latlng);
|
||||||
});
|
});
|
||||||
|
|
||||||
var highResData = Util.getHighResData();
|
var highResData = Util.getHighResData();
|
||||||
|
|
||||||
L.tileLayer(tileUrl, {
|
L.tileLayer(tileUrl, {
|
||||||
attribution: tileAttribution,
|
attribution: tileAttribution,
|
||||||
subdomains: '1234',
|
subdomains: tileSubdomains,
|
||||||
ppi: highResData.ppi,
|
ppi: highResData.ppi,
|
||||||
tileSize: highResData.tileSize,
|
tileSize: highResData.tileSize,
|
||||||
zoomOffset: highResData.zoomOffset,
|
zoomOffset: highResData.zoomOffset,
|
||||||
minZoom: highResData.minZoom,
|
minZoom: highResData.minZoom,
|
||||||
maxZoom: highResData.maxZoom
|
maxZoom: highResData.maxZoom
|
||||||
}).addTo(MapEditor.map);
|
}).addTo(LMapWrapper.map);
|
||||||
|
|
||||||
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
if (mapId) {
|
||||||
|
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MapEditor.markers = L.markerClusterGroup({
|
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
|
maxClusterRadius: 50
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
LMapWrapper.markers.clearLayers();
|
||||||
|
}
|
||||||
|
|
||||||
for (var placeId in places) {
|
for (var placeId in places) {
|
||||||
if (!places.hasOwnProperty(placeId)) {
|
if (!places.hasOwnProperty(placeId)) {
|
||||||
@@ -386,18 +427,229 @@
|
|||||||
var place = places[placeId];
|
var place = places[placeId];
|
||||||
|
|
||||||
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
|
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
|
||||||
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
|
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
|
||||||
zIndexOffset: 1000
|
zIndexOffset: 1000
|
||||||
})
|
})
|
||||||
.addTo(MapEditor.markers)
|
.addTo(LMapWrapper.markers)
|
||||||
.on('click', function () {
|
.on('click', function () {
|
||||||
MapEditor.select(this);
|
MapEditor.select(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
marker.placeId = place.id;
|
marker.placeId = placeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
MapEditor.map.addLayer(MapEditor.markers);
|
LMapWrapper.map.addLayer(LMapWrapper.markers);
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: check whether marker is already existing on the map for the coordinates
|
||||||
|
// or alternatively block saving for matching coordinates
|
||||||
|
placeMarker: function (latLng) {
|
||||||
|
var marker = L.marker(latLng, {
|
||||||
|
icon: LMapWrapper.iconCollection.iconBlue,
|
||||||
|
zIndexOffset: 2000
|
||||||
|
})
|
||||||
|
.addTo(LMapWrapper.markers)
|
||||||
|
.on('click', function () {
|
||||||
|
MapEditor.select(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
MapEditor.select(marker);
|
||||||
|
},
|
||||||
|
|
||||||
|
panTo: function (latLng) {
|
||||||
|
LMapWrapper.map.panTo(latLng);
|
||||||
|
},
|
||||||
|
|
||||||
|
resize: function () {
|
||||||
|
LMapWrapper.map.invalidateSize(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
changeMarkerIcon: function (marker, icon) {
|
||||||
|
marker.setIcon(icon);
|
||||||
|
marker.setZIndexOffset(2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMarker: function (marker) {
|
||||||
|
LMapWrapper.markers.removeLayer(marker);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleStreetViewCover: function () { },
|
||||||
|
|
||||||
|
getSearchRadius: function (location) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var GMapWrapper = {
|
||||||
|
map: null,
|
||||||
|
markers: null,
|
||||||
|
divId: null,
|
||||||
|
streetViewCover: null,
|
||||||
|
streetViewCoverOn: false,
|
||||||
|
iconCollection: {
|
||||||
|
iconGreen: {
|
||||||
|
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
|
||||||
|
scaledSize: new google.maps.Size(24, 32), // scaled size
|
||||||
|
origin: new google.maps.Point(0, 0), // origin
|
||||||
|
anchor: new google.maps.Point(12, 32) // anchor
|
||||||
|
},
|
||||||
|
iconRed: {
|
||||||
|
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
|
||||||
|
scaledSize: new google.maps.Size(24, 32), // scaled size
|
||||||
|
origin: new google.maps.Point(0, 0), // origin
|
||||||
|
anchor: new google.maps.Point(12, 32) // anchor
|
||||||
|
},
|
||||||
|
iconBlue: {
|
||||||
|
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
|
||||||
|
scaledSize: new google.maps.Size(24, 32), // scaled size
|
||||||
|
origin: new google.maps.Point(0, 0), // origin
|
||||||
|
anchor: new google.maps.Point(12, 32) // anchor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function (divId, places) {
|
||||||
|
document.getElementById(divId).style.display = "block";
|
||||||
|
|
||||||
|
if (!GMapWrapper.map) {
|
||||||
|
GMapWrapper.divId = divId;
|
||||||
|
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
|
||||||
|
center: { lat: 0., lng: 0. },
|
||||||
|
zoom: 2,
|
||||||
|
fullscreenControl: false,
|
||||||
|
zoomControl: true,
|
||||||
|
zoomControlOptions: {
|
||||||
|
position: google.maps.ControlPosition.LEFT_BOTTOM
|
||||||
|
},
|
||||||
|
streetViewControl: false,
|
||||||
|
draggableCursor: 'crosshair'
|
||||||
|
});
|
||||||
|
|
||||||
|
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
|
||||||
|
|
||||||
|
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
|
||||||
|
GMapWrapper.placeMarker({
|
||||||
|
lat: mapsMouseEvent.latLng.lat(),
|
||||||
|
lng: mapsMouseEvent.latLng.lng()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mapId) {
|
||||||
|
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GMapWrapper.loadMarkers(places);
|
||||||
|
|
||||||
|
document.getElementById('streetViewCoverSelector').style.display = 'block'
|
||||||
|
},
|
||||||
|
|
||||||
|
hide: function () {
|
||||||
|
document.getElementById(GMapWrapper.divId).style.display = 'none';
|
||||||
|
},
|
||||||
|
|
||||||
|
loadMarkers: function (places) {
|
||||||
|
if (!GMapWrapper.markers) {
|
||||||
|
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
|
||||||
|
imagePath: STATIC_ROOT + '/img/markers/m',
|
||||||
|
imageExtension: 'png?rev' + REVISION
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
GMapWrapper.markers.clearMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var placeId in places) {
|
||||||
|
if (!places.hasOwnProperty(placeId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var place = places[placeId];
|
||||||
|
|
||||||
|
var marker = new google.maps.Marker({
|
||||||
|
position: {
|
||||||
|
lat: place.lat,
|
||||||
|
lng: place.lng
|
||||||
|
},
|
||||||
|
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
|
||||||
|
marker.setLatLng = function (coords) { this.setPosition(coords) };
|
||||||
|
|
||||||
|
marker.addListener('click', function () {
|
||||||
|
MapEditor.select(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.placeId = placeId;
|
||||||
|
|
||||||
|
GMapWrapper.markers.addMarker(marker);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: check whether marker is already existing on the map for the coordinates
|
||||||
|
// or alternatively block saving for matching coordinates
|
||||||
|
placeMarker: function (latLng) {
|
||||||
|
var marker = new google.maps.Marker({
|
||||||
|
map: GMapWrapper.map,
|
||||||
|
position: latLng,
|
||||||
|
icon: GMapWrapper.iconCollection.iconBlue,
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
|
||||||
|
marker.setLatLng = function (coords) { this.setPosition(coords) };
|
||||||
|
|
||||||
|
marker.addListener('click', function () {
|
||||||
|
MapEditor.select(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
GMapWrapper.markers.addMarker(marker);
|
||||||
|
|
||||||
|
MapEditor.select(marker);
|
||||||
|
},
|
||||||
|
|
||||||
|
panTo: function (latLng) {
|
||||||
|
GMapWrapper.map.panTo(latLng);
|
||||||
|
},
|
||||||
|
|
||||||
|
resize: function () {
|
||||||
|
google.maps.event.trigger(GMapWrapper.map, 'resize');
|
||||||
|
},
|
||||||
|
|
||||||
|
changeMarkerIcon: function (marker, icon) {
|
||||||
|
marker.setIcon(icon);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMarker: function (marker) {
|
||||||
|
GMapWrapper.markers.removeMarker(marker);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleStreetViewCover: function () {
|
||||||
|
if (GMapWrapper.streetViewCoverOn) {
|
||||||
|
GMapWrapper.streetViewCover.setMap(null);
|
||||||
|
GMapWrapper.streetViewCoverOn = false;
|
||||||
|
} else {
|
||||||
|
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
|
||||||
|
GMapWrapper.streetViewCoverOn = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getSearchRadius: function (location) {
|
||||||
|
// source: https://www.yorku.ca/mack/CHI01.htm
|
||||||
|
var movementOffset = 4;
|
||||||
|
|
||||||
|
// source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1
|
||||||
|
var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom());
|
||||||
|
|
||||||
|
var minSearchRadius = 5;
|
||||||
|
|
||||||
|
var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel));
|
||||||
|
|
||||||
|
return searchRadius;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// initialize content of #map with google maps
|
||||||
|
MapEditor.map = GMapWrapper;
|
||||||
|
MapEditor.map.init('gmap', places);
|
||||||
|
|
||||||
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
||||||
// switch off fullscreenControl because positioning doesn't work
|
// switch off fullscreenControl because positioning doesn't work
|
||||||
@@ -440,4 +692,48 @@
|
|||||||
document.getElementById('deleteButton').onclick = function () {
|
document.getElementById('deleteButton').onclick = function () {
|
||||||
MapEditor.deletePlace();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
@@ -59,10 +59,25 @@ def getRevisionForRef(ref):
|
|||||||
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
||||||
|
|
||||||
def getLatestReleaseTag():
|
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():
|
def updateRepoFromRemote():
|
||||||
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
|
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
|
||||||
|
|
||||||
def checkoutWorktree(worktreePath, ref):
|
def checkoutWorktree(worktreePath, ref):
|
||||||
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
||||||
|
|||||||
+4
-1
@@ -16,7 +16,7 @@ echo "Installing Yarn packages..."
|
|||||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||||
|
|
||||||
echo "Installing MapGuesser DB..."
|
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..."
|
echo "Migrating DB..."
|
||||||
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
(cd ${ROOT_DIR} && ./mapg db:migrate)
|
||||||
@@ -27,6 +27,9 @@ if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
|||||||
|
|
||||||
echo "Linking view files..."
|
echo "Linking view files..."
|
||||||
(cd ${ROOT_DIR} && ./mapg view:link)
|
(cd ${ROOT_DIR} && ./mapg view:link)
|
||||||
|
else
|
||||||
|
echo "Creating the first user..."
|
||||||
|
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
touch ${ROOT_DIR}/installed
|
touch ${ROOT_DIR}/installed
|
||||||
@@ -27,7 +27,7 @@ class AddUserCommand extends Command
|
|||||||
$user->setActive(true);
|
$user->setActive(true);
|
||||||
$user->setCreatedDate(new DateTime());
|
$user->setCreatedDate(new DateTime());
|
||||||
|
|
||||||
if ($input->hasArgument('type')) {
|
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
|
||||||
$user->setType($input->getArgument('type'));
|
$user->setType($input->getArgument('type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use MapGuesser\PersistentData\PersistentDataManager;
|
|||||||
use MapGuesser\Repository\MultiRoomRepository;
|
use MapGuesser\Repository\MultiRoomRepository;
|
||||||
use MapGuesser\Repository\UserConfirmationRepository;
|
use MapGuesser\Repository\UserConfirmationRepository;
|
||||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||||
|
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||||
use MapGuesser\Repository\UserRepository;
|
use MapGuesser\Repository\UserRepository;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -25,6 +26,8 @@ class MaintainDatabaseCommand extends Command
|
|||||||
|
|
||||||
private MultiRoomRepository $multiRoomRepository;
|
private MultiRoomRepository $multiRoomRepository;
|
||||||
|
|
||||||
|
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
@@ -34,6 +37,7 @@ class MaintainDatabaseCommand extends Command
|
|||||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||||
$this->multiRoomRepository = new MultiRoomRepository();
|
$this->multiRoomRepository = new MultiRoomRepository();
|
||||||
|
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure(): void
|
public function configure(): void
|
||||||
@@ -81,6 +85,10 @@ class MaintainDatabaseCommand extends Command
|
|||||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||||
|
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||||
|
}
|
||||||
|
|
||||||
$this->pdm->deleteFromDb($user);
|
$this->pdm->deleteFromDb($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ class GameController
|
|||||||
'rounds' => [],
|
'rounds' => [],
|
||||||
'currentRound' => -1
|
'currentRound' => -1
|
||||||
]);
|
]);
|
||||||
|
} else { // update the area of the map in the session in any case
|
||||||
|
$state['area'] = $map->getArea();
|
||||||
|
$session->set('state', $state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonContent([
|
return new JsonContent([
|
||||||
@@ -111,6 +114,11 @@ class GameController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
$room = $this->multiRoomRepository->getByRoomId($roomId);
|
||||||
|
|
||||||
|
if(!isset($room)) {
|
||||||
|
return new JsonContent(['error' => 'game_not_found']);
|
||||||
|
}
|
||||||
|
|
||||||
$state = $room->getStateArray();
|
$state = $room->getStateArray();
|
||||||
$map = $this->mapRepository->getById($state['mapId']);
|
$map = $this->mapRepository->getById($state['mapId']);
|
||||||
$token = $this->getMultiToken($roomId);
|
$token = $this->getMultiToken($roomId);
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ use MapGuesser\Response\JsonContent;
|
|||||||
use MapGuesser\Interfaces\Response\IContent;
|
use MapGuesser\Interfaces\Response\IContent;
|
||||||
use MapGuesser\Multi\MultiConnector;
|
use MapGuesser\Multi\MultiConnector;
|
||||||
use MapGuesser\PersistentData\PersistentDataManager;
|
use MapGuesser\PersistentData\PersistentDataManager;
|
||||||
|
use MapGuesser\PersistentData\Model\UserPlayedPlace;
|
||||||
use MapGuesser\Repository\MultiRoomRepository;
|
use MapGuesser\Repository\MultiRoomRepository;
|
||||||
use MapGuesser\Repository\PlaceRepository;
|
use MapGuesser\Repository\PlaceRepository;
|
||||||
|
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||||
|
|
||||||
class GameFlowController
|
class GameFlowController
|
||||||
{
|
{
|
||||||
@@ -25,6 +27,8 @@ class GameFlowController
|
|||||||
|
|
||||||
private PlaceRepository $placeRepository;
|
private PlaceRepository $placeRepository;
|
||||||
|
|
||||||
|
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||||
|
|
||||||
public function __construct(IRequest $request)
|
public function __construct(IRequest $request)
|
||||||
{
|
{
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
@@ -32,6 +36,7 @@ class GameFlowController
|
|||||||
$this->multiConnector = new MultiConnector();
|
$this->multiConnector = new MultiConnector();
|
||||||
$this->multiRoomRepository = new MultiRoomRepository();
|
$this->multiRoomRepository = new MultiRoomRepository();
|
||||||
$this->placeRepository = new PlaceRepository();
|
$this->placeRepository = new PlaceRepository();
|
||||||
|
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function initialData(): IContent
|
public function initialData(): IContent
|
||||||
@@ -145,9 +150,32 @@ class GameFlowController
|
|||||||
|
|
||||||
$session->set('state', $state);
|
$session->set('state', $state);
|
||||||
|
|
||||||
|
$this->saveVisit($last);
|
||||||
|
|
||||||
return new JsonContent($response);
|
return new JsonContent($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save the selected place for the round in UserPlayedPlace
|
||||||
|
private function saveVisit($last): void
|
||||||
|
{
|
||||||
|
$session = $this->request->session();
|
||||||
|
$userId = $session->get('userId');
|
||||||
|
|
||||||
|
if(isset($userId)) {
|
||||||
|
$placeId = $last['placeId'];
|
||||||
|
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
|
||||||
|
if(!$userPlayedPlace) {
|
||||||
|
$userPlayedPlace = new UserPlayedPlace();
|
||||||
|
$userPlayedPlace->setUserId($userId);
|
||||||
|
$userPlayedPlace->setPlaceId($placeId);
|
||||||
|
} else {
|
||||||
|
$userPlayedPlace->incrementOccurrences();
|
||||||
|
}
|
||||||
|
$userPlayedPlace->setLastTimeDate(new DateTime());
|
||||||
|
$this->pdm->saveToDb($userPlayedPlace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function multiGuess(): IContent
|
public function multiGuess(): IContent
|
||||||
{
|
{
|
||||||
$roomId = $this->request->query('roomId');
|
$roomId = $this->request->query('roomId');
|
||||||
@@ -224,7 +252,10 @@ class GameFlowController
|
|||||||
|
|
||||||
private function startNewGame(array &$state, int $mapId): void
|
private function startNewGame(array &$state, int $mapId): void
|
||||||
{
|
{
|
||||||
$places = $this->placeRepository->getRandomNForMapWithValidPano($mapId, static::NUMBER_OF_ROUNDS);
|
$session = $this->request->session();
|
||||||
|
$userId = $session->get('userId');
|
||||||
|
|
||||||
|
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
|
||||||
|
|
||||||
$state['rounds'] = [];
|
$state['rounds'] = [];
|
||||||
$state['currentRound'] = 0;
|
$state['currentRound'] = 0;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use MapGuesser\PersistentData\Model\UserPasswordResetter;
|
|||||||
use MapGuesser\PersistentData\PersistentDataManager;
|
use MapGuesser\PersistentData\PersistentDataManager;
|
||||||
use MapGuesser\Repository\UserConfirmationRepository;
|
use MapGuesser\Repository\UserConfirmationRepository;
|
||||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||||
|
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||||
use MapGuesser\Repository\UserRepository;
|
use MapGuesser\Repository\UserRepository;
|
||||||
use MapGuesser\Response\HtmlContent;
|
use MapGuesser\Response\HtmlContent;
|
||||||
use MapGuesser\Response\JsonContent;
|
use MapGuesser\Response\JsonContent;
|
||||||
@@ -32,6 +33,8 @@ class LoginController
|
|||||||
|
|
||||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||||
|
|
||||||
|
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||||
|
|
||||||
public function __construct(IRequest $request)
|
public function __construct(IRequest $request)
|
||||||
{
|
{
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
@@ -39,6 +42,7 @@ class LoginController
|
|||||||
$this->userRepository = new UserRepository();
|
$this->userRepository = new UserRepository();
|
||||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||||
|
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLoginForm()
|
public function getLoginForm()
|
||||||
@@ -430,6 +434,10 @@ class LoginController
|
|||||||
|
|
||||||
$user = $this->userRepository->getById($confirmation->getUserId());
|
$user = $this->userRepository->getById($confirmation->getUserId());
|
||||||
|
|
||||||
|
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||||
|
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||||
|
}
|
||||||
|
|
||||||
$this->pdm->deleteFromDb($user);
|
$this->pdm->deleteFromDb($user);
|
||||||
|
|
||||||
\Container::$dbConnection->commit();
|
\Container::$dbConnection->commit();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use MapGuesser\PersistentData\Model\Place;
|
|||||||
use MapGuesser\PersistentData\PersistentDataManager;
|
use MapGuesser\PersistentData\PersistentDataManager;
|
||||||
use MapGuesser\Repository\MapRepository;
|
use MapGuesser\Repository\MapRepository;
|
||||||
use MapGuesser\Repository\PlaceRepository;
|
use MapGuesser\Repository\PlaceRepository;
|
||||||
|
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||||
use MapGuesser\Response\HtmlContent;
|
use MapGuesser\Response\HtmlContent;
|
||||||
use MapGuesser\Response\JsonContent;
|
use MapGuesser\Response\JsonContent;
|
||||||
use MapGuesser\Util\Geo\Bounds;
|
use MapGuesser\Util\Geo\Bounds;
|
||||||
@@ -27,12 +28,15 @@ class MapAdminController implements ISecured
|
|||||||
|
|
||||||
private PlaceRepository $placeRepository;
|
private PlaceRepository $placeRepository;
|
||||||
|
|
||||||
|
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||||
|
|
||||||
public function __construct(IRequest $request)
|
public function __construct(IRequest $request)
|
||||||
{
|
{
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
$this->pdm = new PersistentDataManager();
|
$this->pdm = new PersistentDataManager();
|
||||||
$this->mapRepository = new MapRepository();
|
$this->mapRepository = new MapRepository();
|
||||||
$this->placeRepository = new PlaceRepository();
|
$this->placeRepository = new PlaceRepository();
|
||||||
|
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
@@ -138,7 +142,7 @@ class MapAdminController implements ISecured
|
|||||||
|
|
||||||
$place = $this->placeRepository->getById((int) $placeRaw['id']);
|
$place = $this->placeRepository->getById((int) $placeRaw['id']);
|
||||||
|
|
||||||
$this->pdm->deleteFromDb($place);
|
$this->deletePlace($place);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,10 +182,19 @@ class MapAdminController implements ISecured
|
|||||||
return new JsonContent(['success' => true]);
|
return new JsonContent(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function deletePlace(Place $place): void
|
||||||
|
{
|
||||||
|
foreach ($this->userPlayedPlaceRepository->getAllByPlace($place) as $userPlayedPlace) {
|
||||||
|
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pdm->deleteFromDb($place);
|
||||||
|
}
|
||||||
|
|
||||||
private function deletePlaces(Map $map): void
|
private function deletePlaces(Map $map): void
|
||||||
{
|
{
|
||||||
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
foreach ($this->placeRepository->getAllForMap($map) as $place) {
|
||||||
$this->pdm->deleteFromDb($place);
|
$this->deletePlace($place);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use MapGuesser\PersistentData\PersistentDataManager;
|
|||||||
use MapGuesser\PersistentData\Model\User;
|
use MapGuesser\PersistentData\Model\User;
|
||||||
use MapGuesser\Repository\UserConfirmationRepository;
|
use MapGuesser\Repository\UserConfirmationRepository;
|
||||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||||
|
use MapGuesser\Repository\UserPlayedPlaceRepository;
|
||||||
use MapGuesser\Response\HtmlContent;
|
use MapGuesser\Response\HtmlContent;
|
||||||
use MapGuesser\Response\JsonContent;
|
use MapGuesser\Response\JsonContent;
|
||||||
use MapGuesser\Response\Redirect;
|
use MapGuesser\Response\Redirect;
|
||||||
@@ -26,12 +27,15 @@ class UserController implements ISecured
|
|||||||
|
|
||||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||||
|
|
||||||
|
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
|
||||||
|
|
||||||
public function __construct(IRequest $request)
|
public function __construct(IRequest $request)
|
||||||
{
|
{
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
$this->pdm = new PersistentDataManager();
|
$this->pdm = new PersistentDataManager();
|
||||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||||
|
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
@@ -201,6 +205,10 @@ class UserController implements ISecured
|
|||||||
$this->pdm->deleteFromDb($userPasswordResetter);
|
$this->pdm->deleteFromDb($userPasswordResetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
|
||||||
|
$this->pdm->deleteFromDb($userPlayedPlace);
|
||||||
|
}
|
||||||
|
|
||||||
$this->pdm->deleteFromDb($user);
|
$this->pdm->deleteFromDb($user);
|
||||||
|
|
||||||
\Container::$dbConnection->commit();
|
\Container::$dbConnection->commit();
|
||||||
@@ -210,7 +218,7 @@ class UserController implements ISecured
|
|||||||
return new JsonContent(['success' => true]);
|
return new JsonContent(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, string &$error): bool
|
private function confirmUserIdentity(User $user, ?DateTime $authenticatedWithGoogleUntil, ?string $password, ?string &$error): bool
|
||||||
{
|
{
|
||||||
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
|
if ($authenticatedWithGoogleUntil !== null && $authenticatedWithGoogleUntil > new DateTime()) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Select
|
|||||||
|
|
||||||
const CONDITION_HAVING = 1;
|
const CONDITION_HAVING = 1;
|
||||||
|
|
||||||
|
const DERIVED_TABLE_KEY = 'DERIVED';
|
||||||
|
|
||||||
private IConnection $connection;
|
private IConnection $connection;
|
||||||
|
|
||||||
private string $table;
|
private string $table;
|
||||||
@@ -55,6 +57,11 @@ class Select
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setDerivedTableAlias(string $tableAlias): Select
|
||||||
|
{
|
||||||
|
return $this->setTableAliases([Select::DERIVED_TABLE_KEY => $tableAlias]);
|
||||||
|
}
|
||||||
|
|
||||||
public function from(string $table): Select
|
public function from(string $table): Select
|
||||||
{
|
{
|
||||||
$this->table = $table;
|
$this->table = $table;
|
||||||
@@ -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
|
private function addJoin(string $type, $table, $column1, string $relation, $column2): void
|
||||||
{
|
{
|
||||||
$this->joins[] = [$type, $table, $column1, $relation, $column2];
|
$this->joins[] = [$type, $table, $column1, $relation, $column2];
|
||||||
@@ -211,10 +223,14 @@ class Select
|
|||||||
|
|
||||||
private function generateQuery(): array
|
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) {
|
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) {
|
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
|
||||||
@@ -245,20 +261,32 @@ class Select
|
|||||||
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
|
$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];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateTable($table, bool $defineAlias = false): string
|
return [$queryString, array_merge($tableParams, $joinParams, $whereParams, $havingParams)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateTable($table, bool $defineAlias = false): array
|
||||||
{
|
{
|
||||||
|
$params = [];
|
||||||
|
|
||||||
if ($table instanceof RawExpression) {
|
if ($table instanceof RawExpression) {
|
||||||
return (string) $table;
|
return [(string) $table, $params];
|
||||||
|
}
|
||||||
|
|
||||||
|
if($table instanceof Select)
|
||||||
|
{
|
||||||
|
return $table->generateQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($this->tableAliases[$table])) {
|
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
|
private function generateColumn($column): string
|
||||||
@@ -271,7 +299,8 @@ class Select
|
|||||||
$out = '';
|
$out = '';
|
||||||
|
|
||||||
if ($column[0]) {
|
if ($column[0]) {
|
||||||
$out .= $this->generateTable($column[0]) . '.';
|
list($tableName, $params) = $this->generateTable($column[0]);
|
||||||
|
$out .= $tableName . '.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$out .= Utils::backtick($column[1]);
|
$out .= Utils::backtick($column[1]);
|
||||||
@@ -297,15 +326,19 @@ class Select
|
|||||||
return implode(',', $columns);
|
return implode(',', $columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateJoins(): string
|
private function generateJoins(): array
|
||||||
{
|
{
|
||||||
$joins = $this->joins;
|
|
||||||
|
|
||||||
array_walk($joins, function (&$value, $key) {
|
$joinQueries = [];
|
||||||
$value = $value[0] . ' JOIN ' . $this->generateTable($value[1], true) . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]);
|
$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(int $type): array
|
private function generateConditions(int $type): array
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class Place extends Model
|
|||||||
return $this->pov->getZoom();
|
return $this->pov->getZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFreshPanoId(bool $canBeIndoor = false): ?string
|
public function getFreshPanoId(): ?string
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
$this->panoIdCachedTimestamp !== null &&
|
$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 = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
|
||||||
$request->setQuery([
|
$request->setQuery([
|
||||||
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
|
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
|
||||||
'location' => $this->position->getLat() . ',' . $this->position->getLng(),
|
'location' => $this->position->getLat() . ',' . $this->position->getLng()
|
||||||
'source' => $canBeIndoor ? 'default' : 'outdoor'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $request->send();
|
$response = $request->send();
|
||||||
$panoData = json_decode($response->getBody(), true);
|
$panoData = json_decode($response->getBody(), true);
|
||||||
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
|
$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->panoIdCached = $panoId;
|
||||||
$this->panoIdCachedTimestamp = new DateTime();
|
$this->panoIdCachedTimestamp = new DateTime();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,13 +28,35 @@ class PlaceRepository
|
|||||||
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
|
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: use Map and User instead of id
|
||||||
|
public function getRandomNPlaces(int $mapId, int $n, ?int $userId): array
|
||||||
|
{
|
||||||
|
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
|
//TODO: use Map instead of id
|
||||||
public function getRandomNForMapWithValidPano(int $mapId, int $n, array $exclude = []): array
|
private function getRandomNForMapWithValidPano(int $mapId, int $n): array
|
||||||
{
|
{
|
||||||
$places = [];
|
$places = [];
|
||||||
|
|
||||||
|
$select = new Select(\Container::$dbConnection, 'places');
|
||||||
|
$select->where('map_id', '=', $mapId);
|
||||||
|
$numberOfPlaces = $select->count();
|
||||||
|
|
||||||
|
$exclude = [];
|
||||||
for ($i = 1; $i <= $n; ++$i) {
|
for ($i = 1; $i <= $n; ++$i) {
|
||||||
$place = $this->getRandomForMapWithValidPano($mapId, $exclude);
|
$place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude);
|
||||||
|
|
||||||
$places[] = $place;
|
$places[] = $place;
|
||||||
$exclude[] = $place->getId();
|
$exclude[] = $place->getId();
|
||||||
@@ -43,11 +65,15 @@ class PlaceRepository
|
|||||||
return $places;
|
return $places;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: use Map instead of id
|
private function getRandomForMapWithValidPano(int $numberOfPlaces, Select $select, array &$exclude, ?callable $pickRandomInt = null): ?Place
|
||||||
public function getRandomForMapWithValidPano(int $mapId, array $exclude = []): Place
|
|
||||||
{
|
{
|
||||||
do {
|
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();
|
$panoId = $place->getFreshPanoId();
|
||||||
if ($panoId === null) {
|
if ($panoId === null) {
|
||||||
@@ -58,25 +84,97 @@ class PlaceRepository
|
|||||||
return $place;
|
return $place;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function selectRandomFromDbForMap(int $mapId, array $exclude): Place
|
private function selectRandomFromDbForMap(int $numberOfPlacesLeft, Select $select, array $exclude, ?callable $pickRandomInt): ?Place
|
||||||
{
|
{
|
||||||
//TODO: omit table name here
|
if ($numberOfPlacesLeft <= 0)
|
||||||
$select = new Select(\Container::$dbConnection, 'places');
|
return null;
|
||||||
$select->where('id', 'NOT IN', $exclude);
|
|
||||||
$select->where('map_id', '=', $mapId);
|
|
||||||
|
|
||||||
$numberOfPlaces = $select->count();
|
if (!isset($pickRandomInt)) {
|
||||||
|
$randomOffset = random_int(0, $numberOfPlacesLeft - 1);
|
||||||
//TODO: prevent this
|
} else {
|
||||||
if ($numberOfPlaces === 0) {
|
$randomOffset = $pickRandomInt($numberOfPlacesLeft);
|
||||||
throw new \Exception('There is no selectable place (count was 0).');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$randomOffset = random_int(0, $numberOfPlaces - 1);
|
$select->where('id', 'NOT IN', $exclude);
|
||||||
|
|
||||||
$select->orderBy('id');
|
|
||||||
$select->limit(1, $randomOffset);
|
$select->limit(1, $randomOffset);
|
||||||
|
|
||||||
return $this->pdm->selectFromDb($select, Place::class);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,22 @@
|
|||||||
@js(node_modules/leaflet/dist/leaflet.js)
|
@js(node_modules/leaflet/dist/leaflet.js)
|
||||||
@js(node_modules/leaflet.markercluster/dist/leaflet.markercluster.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://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)
|
@js(js/map_editor.js)
|
||||||
|
|
||||||
@extends(templates/layout_full)
|
@extends(templates/layout_full)
|
||||||
|
|
||||||
@section(subheader)
|
@section(subheader)
|
||||||
<span><a href="javascript:;" id="mapName" title="Edit map data"><?= $mapName ?></a></span><!--
|
<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" id="jumpCoordinates" placeholder="Insert coordinates here" />
|
||||||
|
<button id="jumpButton" disabled >Jump</button>
|
||||||
|
</span><!--
|
||||||
--><span><!--
|
--><span><!--
|
||||||
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
|
<?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">
|
--><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -53,7 +63,20 @@
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section(main)
|
@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="panorama"></div>
|
||||||
<div id="noPano">
|
<div id="noPano">
|
||||||
<p class="bold">No panorama is available for this location.</p>
|
<p class="bold">No panorama is available for this location.</p>
|
||||||
@@ -71,6 +94,7 @@
|
|||||||
@section(pageScript)
|
@section(pageScript)
|
||||||
<script>
|
<script>
|
||||||
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
|
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
|
||||||
|
var tileSubdomains = '<?= $_ENV['LEAFLET_TILESERVER_SUBDOMAINS'] ?>';
|
||||||
var tileAttribution = '<?= $_ENV['LEAFLET_TILESERVER_ATTRIBUTION'] ?>';
|
var tileAttribution = '<?= $_ENV['LEAFLET_TILESERVER_ATTRIBUTION'] ?>';
|
||||||
var mapId = <?= $mapId ?>;
|
var mapId = <?= $mapId ?>;
|
||||||
var mapBounds = <?= json_encode($bounds) ?>;
|
var mapBounds = <?= json_encode($bounds) ?>;
|
||||||
|
|||||||
@@ -59,6 +59,24 @@
|
|||||||
<button id="startNewGameButton" class="fullWidth">Play this map again</button>
|
<button id="startNewGameButton" class="fullWidth">Play this map again</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="navigation" class="circleControl">
|
||||||
|
<div id="compassContainer" class="controlItem">
|
||||||
|
<div>
|
||||||
|
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/circle_background.svg?rev=<?= REVISION ?>" alt="Circle Background" class="controlBackground" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img id="compass" src="<?= $_ENV['STATIC_ROOT'] ?>/img/compass.svg?rev=<?= REVISION ?>" alt="compass icon" class="controlIcon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="returnToStart" class="controlItem">
|
||||||
|
<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/house-fill.svg?rev=<?= REVISION ?>" alt="Return to Start icon" class="controlIcon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section(pageScript)
|
@section(pageScript)
|
||||||
|
|||||||
@@ -29,6 +29,6 @@
|
|||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!--
|
<p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!--
|
||||||
--><p>© Pőcze Bence <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
|
--><p>© The MapGuesser Contributors <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
|
||||||
</footer>
|
</footer>
|
||||||
@endsection
|
@endsection
|
||||||
Reference in new issue
Block a user