Compare commits

...
Author SHA1 Message Date
bence d30ec3a3a0 Merge pull request 'feature/MAPG-242-add-captcha-for-signup-and-password-reset' (!54) from feature/MAPG-242-add-captcha-for-signup-and-password-reset into develop
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/54
2022-05-26 18:47:46 +02:00
bence cc19d454fa MAPG-242 add captcha validation for password reset
mapguesser/pipeline/pr-develop This commit looks good
2022-05-26 18:45:16 +02:00
bence 241d2f2b30 MAPG-242 add captcha validation for signup 2022-05-26 18:41:26 +02:00
bence d0751017db MAPG-242 add possibility to captcha validation 2022-05-26 18:39:33 +02:00
bence 6014e4517a Merge pull request 'MAPG-241 use class 'text' for text inputs' (!53) from bugfix/MAPG-241-fix-text-input-styles into develop
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/53
2022-05-26 15:58:22 +02:00
bence 81a8153866 MAPG-241 use class 'text' for text inputs
mapguesser/pipeline/pr-develop This commit looks good
2022-05-26 15:55:45 +02:00
bence e435ef6be0 Merge pull request 'feature/MAPG-231-make-it-possible-to-create-unlisted-maps' (!52) from feature/MAPG-231-make-it-possible-to-create-unlisted-maps into develop
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/52
2022-05-26 14:34:48 +02:00
bence 90528e61e5 MAPG-231 cleanup input styles
mapguesser/pipeline/pr-develop This commit looks good
2022-05-26 14:23:29 +02:00
bence d02aa4abe0 MAPG-231 introduce unlisted maps 2022-05-26 14:23:08 +02:00
bence 4f45e213c3 Merge pull request 'add badge for passing builds' (!51) from feature/add-badge-for-builds into develop
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/51
2021-12-22 02:26:03 +01:00
bence d877d2fe03 add badge for passing builds
mapguesser/pipeline/head This commit looks good
mapguesser/pipeline/pr-develop This commit looks good
2021-12-22 02:21:22 +01:00
bence 66240c2d65 Merge pull request 'add Jenkinsfile and Dockerfile-test' (!50) from feature/add-jenkins-support into develop
mapguesser/pipeline/head This commit looks good
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/50
2021-12-22 02:14:44 +01:00
bence 0654b65940 add Jenkinsfile and Dockerfile-test
mapguesser/pipeline/pr-develop This commit looks good
default-pipeline default-pipeline #18
2021-12-22 01:59:31 +01:00
bence 272ec3568d Merge pull request 'bugfix/MAPG-240-fix-static-code-analysis-errors' (!49) from bugfix/MAPG-240-fix-static-code-analysis-errors into develop
default-pipeline default-pipeline #11
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/49
Reviewed-by: Balázs Vigh <balazs@vigh.eu>
2021-05-29 10:22:20 +02:00
bence be4a2038e5 MAPG-240 add use for PlaceInChallenge (used in MapAdminController::deleteChallenge)
default-pipeline default-pipeline #9
2021-05-29 00:57:59 +02:00
bence f305c97513 MAPG-240 UserInChallengeRepository::isUserParticipatingInChallenge should return bool 2021-05-29 00:56:43 +02:00
balazs 784037de6f Merge pull request 'feature/MAPG-235-basic-challenge-mode' (#48) from feature/MAPG-235-basic-challenge-mode into develop
default-pipeline default-pipeline #8
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/48
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-28 20:41:08 +02:00
balazs 6f27450423 MAPG-235 replaced explicit margin-top and margin-bottom with marginTop and marginBottom classes 2021-05-28 08:36:45 +02:00
balazs bbaa2fe1eb MAPG-235 removed font-family and font-weight css attributes for restrictions and now handled on higher level 2021-05-28 08:19:35 +02:00
balazs 28165d76d3 MAPG-235 refactored challenge token generation and check 2021-05-28 08:07:02 +02:00
balazs 1c1e5f051d MAPG-235 simplified highscore calculation 2021-05-26 08:16:32 +02:00
balazs 283c214c50 MAPG-235 noZoom label fix 2021-05-26 07:49:05 +02:00
balazs 7970927654 MAPG-235 refactored whitespaces 2021-05-22 21:17:49 +02:00
balazs 567602c749 MAPG-235 hide restrictions information if there are no restrictions for the challenge 2021-05-22 21:00:54 +02:00
balazs a5238234d2 MAPG-235 information of the current restrictions displayed on the ribbon in the top 2021-05-22 20:54:58 +02:00
balazs c965713c9c Merge branch 'develop' of gitea.e5tv.hu:esoko/mapguesser into feature/MAPG-235-basic-challenge-mode 2021-05-22 16:20:59 +02:00
balazs b00fe4ebe3 MAPG-235 highscores added to the end of challenge 2021-05-22 11:34:55 +02:00
balazs 599d8bf153 MAPG-235 fix unwanted time limit after page reload 2021-05-21 15:41:17 +02:00
balazs 636c47366a MAPG-235 fix loading other players' history and calculating final score 2021-05-21 15:40:43 +02:00
balazs 4520d11559 MAPG-235 fix calculating timelimit when 0 sec time was left from the last round 2021-05-21 14:45:40 +02:00
balazs 5f0b639fe7 MAPG-235 distance and score info added also to the player's own markers 2021-05-21 14:44:28 +02:00
balazs d4a279d2f4 MAPG-235 refactored human readable time format 2021-05-21 11:51:47 +02:00
balazs aadeda05af MAPG-235 timeLimit restored after reload of the page. Time limit selector usability improved 2021-05-21 11:30:30 +02:00
balazs b78d564f6f MAPG-235 time limit restriction for the whole game feature added 2021-05-20 21:32:01 +02:00
balazs c2df9b7713 MAPG-235 timelimit restriction per round functionality implemented for challenges 2021-05-20 20:55:44 +02:00
balazs bbb66ca979 MAPG-235 challenge token related error handling 2021-05-20 08:31:52 +02:00
balazs 3b98570f6d MAPG-235 noMove, noZoom, noPan restrictions implemented 2021-05-19 21:16:59 +02:00
balazs 77ff175794 MAPG-235 added radio button for time limit type and fixed saving restrictions for challenge 2021-05-19 17:55:04 +02:00
balazs ae7fd98256 MAPG-235 allow only logged in users to start a challenge 2021-05-19 17:11:49 +02:00
balazs 517d758e8e MAPG-235 fixed place/map deletion 2021-05-19 16:33:18 +02:00
balazs 2a1b51b014 MAPG-235 added time limit type field to the challenge 2021-05-19 14:52:49 +02:00
balazs c085a5a1ca MAPG-235 fix user deletion bug 2021-05-19 13:52:19 +02:00
balazs a7cfc3c19f MAPG-235 queries with selected relations enabled; reduced size of some of the queries 2021-05-19 13:06:46 +02:00
balazs 7898c52328 MAPG-235 go back to start button added to the end of the challenge 2021-05-18 20:22:43 +02:00
balazs 62251b1062 MAPG-235 created error handling for challenge access without login 2021-05-18 19:55:05 +02:00
balazs 069c6b37c8 MAPG-235 fixed bugs with displaying score and round 2021-05-18 18:06:02 +02:00
balazs 30f4b7ad19 MAPG-235 fixed sending history to initializer and fixed pov data format 2021-05-17 21:30:07 +02:00
balazs 45ddb7f56a MAPG-235 reduced number of queries for init and guess in challenge 2021-05-16 20:46:53 +02:00
balazs 7792f1c3ff MAPG-235 refactor: renamed fields in challenge related tables 2021-05-16 13:14:00 +02:00
balazs 7a1674fdd0 Merge branch 'feature/MAPG-235-basic-challenge-mode' of gitea.e5tv.hu:esoko/mapguesser into feature/MAPG-235-basic-challenge-mode 2021-05-15 11:43:47 +02:00
balazs 44df94eb98 MAPG-235 queries with relations on any recursion level implemented 2021-05-15 11:43:00 +02:00
balazs 00afd65d75 MAP-235 queries with relations on any recursion level implemented 2021-05-15 11:32:41 +02:00
balazs 21e41b7c36 MAP-235 fixed overlapping markers of history and user's guesses 2021-05-13 19:55:32 +02:00
balazs 69964acfb2 MAPG-235 results of other players shown 2021-05-13 19:48:25 +02:00
balazs e7869d67f7 MAPG-235 guesses of other players in the round are sent after guess 2021-05-13 13:06:24 +02:00
balazs 93f8fc3f34 MAPG-235 basic gameflow works 2021-05-12 21:01:41 +02:00
balazs 5daed10036 MAPG-235 challengeInitialData implemented and first round is loaded 2021-05-12 15:34:15 +02:00
balazs d7147b30d6 MAPG-235 new challenge can be created and is prepared 2021-05-12 13:51:06 +02:00
bence c02f595606 Merge pull request 'MAPG-143 make it possible to disable game for guests (default is disable)' (#47) from feature/MAPG-143-allow-game-only-for-users into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/47
2021-05-11 18:29:55 +02:00
balazs 99244e93d2 MAPG-235 updated structure and created models 2021-05-11 11:16:39 +02:00
balazs 313a3568aa MAPG-235 extended database structure for challenges 2021-05-10 21:22:07 +02:00
bence 54bc9c31db MAPG-143 make it possible to disable game for guests (default is disable) 2021-05-10 19:10:09 +02:00
bence 75bea7c05c Merge pull request 'MAPG-229 make .controlItem relative so it's children can be sized more properly' (#45) from bugfix/MAPG-229-circle-controls-won-t-show-up-in-firefox into develop
default-pipeline default-pipeline #204
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/45
2021-05-09 17:22:13 +02:00
bence 1c3238e64e Merge pull request 'MAPG-230 renew session cookie if it already exists' (#44) from bugfix/MAPG-230-session-s-cookie-is-not-renewed into develop
default-pipeline default-pipeline #203
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/44
2021-05-09 17:21:46 +02:00
bence 6cb9e17ddf Merge pull request 'give example for LEAFLET_TILESERVER_ATTRIBUTION' (#46) from feature/extend-leaflet-env-examples into develop
default-pipeline default-pipeline #202
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/46
2021-05-09 17:17:18 +02:00
bence 88ad6e9d61 give example for LEAFLET_TILESERVER_ATTRIBUTION
default-pipeline default-pipeline #197
2021-05-09 17:11:51 +02:00
bence 2fb807c479 MAPG-229 make .controlItem relative so it's children can be sized more properly
default-pipeline default-pipeline #196
2021-05-09 16:15:29 +02:00
bence 8e9e5b08f9 MAPG-230 renew session cookie if it already exists
default-pipeline default-pipeline #194
2021-05-09 15:49:27 +02:00
bence 94624d5b2c Merge pull request 'reverted removal of preference for outdoor panorama in the front end' (#43) from feature/preference-for-outdoor-panorama-in-the-frontend into develop
default-pipeline default-pipeline #195
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/43
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-09 13:34:36 +02:00
balazs 71c728d56c reverted removal of preference for outdoor panorama in the front end
default-pipeline default-pipeline #190
2021-05-09 11:46:03 +02:00
balazs 216d30329f Merge pull request 'feature/avoid-repeating-places-in-game' (#38) from feature/avoid-repeating-places-in-game into develop
default-pipeline default-pipeline #189
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/38
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-09 10:58:53 +02:00
balazs 3f8311d708 implemented review findings
default-pipeline default-pipeline #188
2021-05-08 23:37:36 +02:00
balazs 7f8c1eb291 Merge pull request 'added error handling when requesting to join non existing room' (#41) from bugfix/unhandled-error-when-trying-to-join-non-existing-room into develop
default-pipeline default-pipeline #187
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/41
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:59:29 +02:00
balazs d57a4c63e0 Merge pull request 'check the user type if it has valid value' (#40) from bugfix/cli-user-creation-crashes-when-no-type-is-given into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/40
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:59:09 +02:00
balazs ed8e1773c4 Merge pull request 'allow error string to be null in case the password was correct' (#39) from bugfix/account-cannot-be-deleted into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/39
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:57:37 +02:00
balazs 2963bfe739 Merge pull request 'added leaflet subdomains to the environment variables, which are used now for initializing a leaflet map' (#42) from feature/leaflet-subdomains-in-environment-variables into develop
default-pipeline default-pipeline #184
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/42
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-08 20:57:07 +02:00
balazs 7232b322ab extended readme
default-pipeline default-pipeline #183
2021-05-08 18:30:37 +02:00
balazs e6819b6dd3 added leaflet subdomains to the environment variables, which are used now for initializing a leaflet map
default-pipeline default-pipeline #182
2021-05-08 18:20:54 +02:00
balazs 01a4fdf5e5 added error handling when requesting to join non existing room
default-pipeline default-pipeline #181
2021-05-08 18:03:17 +02:00
balazs 1cc3294ee7 check the user type if it has valid value
default-pipeline default-pipeline #180
2021-05-08 16:19:49 +02:00
balazs ad0b8d078f allow error string to be null in case the password was correct
default-pipeline default-pipeline #178
2021-05-08 09:34:34 +02:00
balazs 899817a853 removed unnecessary comment
default-pipeline default-pipeline #177
2021-05-06 20:41:23 +02:00
balazs c626e36bbb specified explicit type in function parameter
default-pipeline default-pipeline #176
2021-05-06 20:39:00 +02:00
balazs 8d8074977b fixing comments
default-pipeline default-pipeline #175
2021-05-06 20:25:48 +02:00
balazs 8b3c95bdc7 handling of deleting places or user updated
default-pipeline default-pipeline #174
2021-05-06 20:09:05 +02:00
balazs b2535ad78a refactored function and variable names, and replaced variables in inner scope
default-pipeline default-pipeline #173
2021-05-06 17:12:18 +02:00
balazs 886bd02f88 removed mistakenly added parameter
default-pipeline default-pipeline #172
2021-05-06 10:09:28 +02:00
balazs 7143c7ec63 moved saving to UserPlayedPlace to seperate function
default-pipeline default-pipeline #171
2021-05-06 10:05:23 +02:00
balazs fb7c0e7a5c renamed the random int generator function
default-pipeline default-pipeline #170
2021-05-06 09:36:49 +02:00
balazs 8136273b33 replaced left join with inner join for older places search
default-pipeline default-pipeline #169
2021-05-05 21:58:19 +02:00
balazs 3c6a7a3c5f selection with preference for older places; refactored PlaceRepository 2021-05-05 21:50:23 +02:00
balazs 01f3c7c8cf added preference for places not seen by user in the selection of places for the game 2021-05-05 19:19:38 +02:00
balazs 66c871fbc2 Extended Select functionality with handling of derived tables 2021-05-05 19:18:07 +02:00
balazs 807b4d024f extended database with UserPlayedPlace table for tracking when and what place a User played in game 2021-05-05 19:16:54 +02:00
bence 430b32d0c6 Merge pull request 'do not exclude indoor panoramas' (#37) from feature/pano-could-be-indoor into develop
default-pipeline default-pipeline #179
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/37
2021-05-02 18:44:40 +02:00
bence 11eee64895 do not exclude indoor panoramas
default-pipeline default-pipeline #166
2021-05-02 18:38:29 +02:00
balazs cc24f96395 Merge pull request 'radius calculation for new panorama search based on latitude and mercator projection for google maps' (#36) from feature/more-accurate-new-selection-in-mapeditor-on-google-maps into develop
default-pipeline default-pipeline #165
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/36
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 18:02:28 +02:00
balazs c3e5a6db1c radius calculation for new panorama search based on latitude and mercator projection for google maps
default-pipeline default-pipeline #164
2021-05-02 16:57:47 +02:00
bence 398204c5d5 Merge pull request 'feature/place-streetview-cover-to-the-map' (#35) from feature/place-streetview-cover-to-the-map into develop
default-pipeline default-pipeline #162
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/35
2021-05-02 16:27:41 +02:00
bence aaa5a1daba Merge pull request 'bugfix/streetviewcontrol-is-still-visible' (#34) from bugfix/streetviewcontrol-is-still-visible into develop
default-pipeline default-pipeline #161
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/34
2021-05-02 16:27:34 +02:00
bence c112a25409 move street view cover button to the map as circleControl
default-pipeline default-pipeline #160
2021-05-02 15:04:14 +02:00
bence 35cab17c21 make circle control items more general 2021-05-02 15:04:14 +02:00
bence f9c1829a00 enable zoomControl for Leaflet map to be consistent with Google map
default-pipeline default-pipeline #157
2021-05-02 13:45:34 +02:00
bence e03850ce05 switch off streetViewControl explicitly 2021-05-02 13:42:46 +02:00
bence 55ec226407 Merge pull request 'feature/google-maps-in-mapeditor-added' (#33) from feature/google-maps-in-mapeditor-added into develop
default-pipeline default-pipeline #155
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/33
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 13:38:17 +02:00
bence 6ef04fcd4e fit both google and leaflet map to bounds if map already exists
default-pipeline default-pipeline #154
2021-05-02 13:29:48 +02:00
bence 3045d8acbb remove builtin streetViewControl from google map 2021-05-02 13:13:11 +02:00
balazs 19a1b79b0a toggling of street view cover added
default-pipeline default-pipeline #153
2021-05-02 12:56:25 +02:00
balazs d432a5b584 cleaned up the comment 2021-05-02 12:34:57 +02:00
balazs 8b9345eb36 removed unused css declarations; renamed invalidateSize to resize 2021-05-02 12:34:57 +02:00
balazs e1eb0077b1 hide the map selection and jump to location elements on mobile view 2021-05-02 12:34:57 +02:00
balazs 745bda11c0 refactoring and bug fixing related to the map switching 2021-05-02 12:34:57 +02:00
balazs 0bb31cbe14 added switching functionality between maps in mapeditor 2021-05-02 12:34:57 +02:00
balazs ef8ad69506 organised Leaflet map and Google Maps into wrappers 2021-05-02 12:23:37 +02:00
bence f10ad37a74 Merge pull request 'update readme' (#32) from feature/update-readme into develop
default-pipeline default-pipeline #152
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/32
2021-04-29 20:03:19 +02:00
bence 70b60d2fb8 create the first user during installation in development mode 2021-04-29 20:02:00 +02:00
bence 7e68580db6 update readme 2021-04-29 20:01:42 +02:00
balazs 8218883faf Merge pull request 'Feature In-Game Compass' (#27) from feature/in-game-compass into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/27
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 19:15:44 +02:00
balazs 145cb5f637 Merge branch 'develop' into feature/in-game-compass
default-pipeline default-pipeline #151
2021-04-29 19:00:11 +02:00
balazs 6d3f74f911 increased the left margin for mobile view
default-pipeline default-pipeline #150
2021-04-29 18:59:24 +02:00
balazs 3d663fb5d6 Merge pull request 'Quickfix for single player game cannot be restarted' (#30) from bugfix/quickfix-for-single-play-cannot-be-restarted into develop
default-pipeline default-pipeline #149
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/30
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:58 +02:00
balazs 759f654a0d Merge pull request 'feature/return-to-starting-point-button' (#26) from feature/return-to-starting-point-button into develop
default-pipeline default-pipeline #148
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/26
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:22 +02:00
bence c09cce3c97 Merge pull request 'MAPG-167 set background color' (#31) from feature/MAPG-167-set-background-color into develop
default-pipeline default-pipeline #147
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/31
2021-04-29 18:46:26 +02:00
bence 276e331a47 MAPG-167 set background color
default-pipeline default-pipeline #146
2021-04-29 18:43:30 +02:00
balazs 62f4fd7b84 readyToContinue is true by default and set to false only in the multigame initializing
default-pipeline default-pipeline #145
2021-04-29 13:57:32 +02:00
balazs b22f13887f clicking on compass sets the heading direction to north
default-pipeline default-pipeline #144
2021-04-29 12:39:44 +02:00
balazs 2a3f2212a7 replaced span with div for the compass item
default-pipeline default-pipeline #143
2021-04-29 12:12:17 +02:00
balazs 98df08e5fa Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 12:10:02 +02:00
balazs 8f0cfd4489 replaced spans with divs again and fixed the css issue
default-pipeline default-pipeline #142
2021-04-29 12:08:04 +02:00
balazs d42280eb10 only the image is rotated, not the whole div
default-pipeline default-pipeline #141
2021-04-29 09:18:24 +02:00
balazs 0739c65961 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 09:11:32 +02:00
balazs 3122696364 added the visibility reset to resetRound and reverted the span div replacement in the view
default-pipeline default-pipeline #140
2021-04-29 09:07:44 +02:00
balazs 462f6cde80 Merge branch 'feature/return-to-starting-point-button' of gitea.e5tv.hu:esoko/mapguesser into feature/return-to-starting-point-button
default-pipeline default-pipeline #139
2021-04-29 09:05:32 +02:00
balazs 3f4fa83d87 reverted replacing spans with divs 2021-04-29 09:04:50 +02:00
balazs ea599809b8 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 08:34:39 +02:00
balazs f0b0de405c Merge branch 'develop' into feature/return-to-starting-point-button
default-pipeline default-pipeline #138
2021-04-29 08:25:52 +02:00
balazs ffc4aaf110 fixed findings
default-pipeline default-pipeline #137
2021-04-29 08:24:20 +02:00
balazs 08c38c6374 reverted style tailoring for guess
default-pipeline default-pipeline #129
2021-04-28 20:46:52 +02:00
balazs 854775011c tailored the style for the navigation buttons
default-pipeline default-pipeline #128
2021-04-28 20:42:42 +02:00
balazs e33d8c02e9 added compass button and coupled its transform:rotate style attribute with the POV heading value 2021-04-28 20:33:17 +02:00
balazs 3d52c967b6 reformatted: added new line breaks to the game.css
default-pipeline default-pipeline #127
2021-04-28 18:30:58 +02:00
balazs 5c8f3d6fb9 returns to start when clicked on home symbol
default-pipeline default-pipeline #126
2021-04-28 18:26:46 +02:00
balazs b9927f79fc added a new button to the view for the return to starting point feature 2021-04-28 17:58:48 +02:00
62 changed files with 3008 additions and 320 deletions

No files matched your search

+4
View File
@@ -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
@@ -19,3 +20,6 @@ MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000 MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090 MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090 MULTI_WS_PORT=8090
ENABLE_GAME_FOR_GUESTS=0
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret
Vendored
+59
View File
@@ -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'
}
}
}
}
}
+44 -9
View File
@@ -1,5 +1,7 @@
# MapGuesser # MapGuesser
[![Build Status](https://jenkins.e5tv.hu/job/mapguesser/job/develop/badge/icon)](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. 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 ## Installation
@@ -14,7 +16,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 +26,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 +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. 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="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
```
### (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 +85,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;
@@ -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;
+4
View File
@@ -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
+70 -2
View File
@@ -15,6 +15,17 @@
right: 0; right: 0;
background-color: #000000; background-color: #000000;
opacity: 0.5; opacity: 0.5;
z-index: 4;
}
#panningBlockerCover {
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0;
z-index: 3; z-index: 3;
} }
@@ -22,7 +33,7 @@
position: absolute; position: absolute;
bottom: 30px; bottom: 30px;
right: 20px; right: 20px;
z-index: 2; z-index: 3;
} }
#guess.result { #guess.result {
@@ -149,13 +160,58 @@
color: #701919; 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) { @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 +228,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 +275,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 +293,9 @@
right: 20px; right: 20px;
bottom: 30px; bottom: 30px;
} }
#navigation {
bottom: 30px;
left: 10px;
}
} }
} }
+17 -1
View File
@@ -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) {
+77 -13
View File
@@ -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;
@@ -27,11 +31,11 @@ main {
color: #ffffff; 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; font-family: 'Roboto', sans-serif;
} }
h1, h2 { h1, h2, h3 {
font-weight: 500; font-weight: 500;
} }
@@ -51,7 +55,11 @@ h2, header.small h1 {
font-size: 24px; font-size: 24px;
} }
p, h2 { h3 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%; line-height: 150%;
} }
@@ -233,7 +241,7 @@ button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.
background-color: #1b7d31; background-color: #1b7d31;
} }
input, select, textarea { input.text, select, textarea {
background-color: #f9fafb; background-color: #f9fafb;
border: solid #c8d2e1 1px; border: solid #c8d2e1 1px;
border-radius: 2px; border-radius: 2px;
@@ -242,22 +250,26 @@ input, select, textarea {
font-weight: 300; font-weight: 300;
} }
input, select { input.text, select {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
padding: 0 5px; padding: 0 5px;
} }
input[type=checkbox], input[type=radio] {
margin-right: 0.5em;
}
textarea { textarea {
padding: 5px; padding: 5px;
resize: none; 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; font-size: 18px;
} }
input.big, select.big, div.inputWithButton>input { input.text.big, select.big, div.inputWithButton>input.text {
height: 35px; height: 35px;
line-height: 35px; line-height: 35px;
padding: 0 6px; padding: 0 6px;
@@ -272,19 +284,19 @@ input.fullWidth, select.fullWidth, textarea.fullWidth {
width: 100%; width: 100%;
} }
input:disabled, select:disabled, textarea:disabled { input.text:disabled, select:disabled, textarea:disabled {
background-color: #dfdfdf; background-color: #dfdfdf;
border: solid #dfdfdf 1px; border: solid #dfdfdf 1px;
color: #000000; color: #000000;
} }
input:focus, select:focus, textarea:focus { input.text:focus, select:focus, textarea:focus {
background-color: #ffffff; background-color: #ffffff;
border: solid #29457f 2px; border: solid #29457f 2px;
outline: none; outline: none;
} }
input:focus, select:focus { input.text:focus, select:focus {
padding: 0 4px; padding: 0 4px;
} }
@@ -292,16 +304,16 @@ textarea:focus {
padding: 4px; padding: 4px;
} }
input.big:focus, select.big:focus { input.text.big:focus, select.big:focus {
padding: 0 5px; padding: 0 5px;
} }
div.inputWithButton>input { div.inputWithButton>input.text {
width: 100%; width: 100%;
padding: 0 83px 0 6px; padding: 0 83px 0 6px;
} }
div.inputWithButton>input:focus { div.inputWithButton>input.text:focus {
padding: 0 82px 0 5px; padding: 0 82px 0 5px;
} }
@@ -374,6 +386,7 @@ header>p>span:not(:last-child) {
} }
main { main {
background-color: #ffffff;
padding: 6px 12px; padding: 6px 12px;
} }
@@ -438,6 +451,45 @@ div.box {
box-sizing: border-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) { @media screen and (max-width: 599px) {
header h1 span { header h1 span {
display: none; display: none;
@@ -464,6 +516,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 +551,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) {
+8
View File
@@ -13,6 +13,10 @@ div.mapItem.new {
align-items: center; align-items: center;
} }
div.mapItem.unlisted {
opacity: 0.6;
}
div.mapItem>div.title { div.mapItem>div.title {
background-color: #28a745; background-color: #28a745;
color: white; color: white;
@@ -75,6 +79,10 @@ div.mapItem>div.buttonContainer {
grid-auto-flow: column; grid-auto-flow: column;
} }
#timeLimitType {
margin-left: 1.5em;
}
@media screen and (min-width: 1504px) { @media screen and (min-width: 1504px) {
#mapContainer { #mapContainer {
grid-template-columns: auto auto auto auto; grid-template-columns: auto auto auto auto;
+3
View File
@@ -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

+5
View File
@@ -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

+5
View File
@@ -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

+4
View File
@@ -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.
+5
View File
@@ -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

+369 -61
View File
@@ -1,10 +1,13 @@
'use strict'; 'use strict';
const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
(function () { (function () {
var Game = { var Game = {
NUMBER_OF_ROUNDS: 5, NUMBER_OF_ROUNDS: 5,
MAX_SCORE: 1000, MAX_SCORE: 1000,
type: GameType.SINGLE,
mapBounds: null, mapBounds: null,
multi: { token: null, owner: false }, multi: { token: null, owner: false },
rounds: [], rounds: [],
@@ -16,8 +19,10 @@
guessMarker: null, guessMarker: null,
adaptGuess: false, adaptGuess: false,
googleLink: null, googleLink: null,
history: [],
restrictions: null,
readyToContinue: false, readyToContinue: true,
timeoutEnd: null, timeoutEnd: null,
countdownHandler: null, countdownHandler: null,
@@ -97,6 +102,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');
@@ -209,6 +216,19 @@
} }
}, },
getGameIdentifier: function () {
switch (Game.type) {
case GameType.SINGLE:
return '/game/' + mapId;
case GameType.MULTI:
return '/multiGame/' + roomId;
case GameType.CHALLENGE:
return '/challenge/' + challengeToken;
default:
return '/game/' + mapId;
}
},
prepare: function () { prepare: function () {
var data = new FormData(); var data = new FormData();
var userNames; var userNames;
@@ -224,7 +244,7 @@
} }
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json'; var url = Game.getGameIdentifier() + '/prepare.json';
MapGuesser.httpRequest('POST', url, function () { MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
@@ -267,7 +287,7 @@
} }
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () { MapGuesser.httpRequest('POST', Game.getGameIdentifier() + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden'; document.getElementById('panoCover').style.visibility = 'hidden';
@@ -276,24 +296,199 @@
return; return;
} }
Game.panoId = this.response.place.panoId; Game.loadHistory(this.response);
Game.pov = this.response.place.pov;
for (var i = 0; i < this.response.history.length; ++i) { Game.restrictions = this.response.restrictions;
var round = this.response.history[i]; Game.displayRestrictions();
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true); if (this.response.finished) {
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
Game.scoreSum += round.result.score; Game.transitToResultMap();
Game.showSummary();
} else {
Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
Game.startNewRound();
} }
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);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE); document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.startNewRound();
}); });
}, },
enableRestrictions: function () {
if (!Game.restrictions) {
return;
}
Game.panorama.setOptions({
clickToGo: !Game.restrictions.noMove,
linksControl: !(Game.restrictions.noMove || Game.restrictions.noPan),
scrollwheel: !Game.restrictions.noZoom
});
if (Game.restrictions.noPan) {
document.getElementById('panningBlockerCover').style.display = 'block';
}
if (Game.restrictions.timeLimit) {
Game.startCountdown(Game.restrictions.timeLimit, function () {
Game.guess();
});
}
},
displayRestrictions: function () {
if (!Game.restrictions) {
return;
}
var restrictionsForDisplay = [];
if (Game.restrictions.timeLimit) {
restrictionsForDisplay.push('time limit per ' + Game.restrictions.timeLimitType);
}
if (Game.restrictions.noPan) {
restrictionsForDisplay.push('no camera change');
}
else {
if (Game.restrictions.noMove) {
restrictionsForDisplay.push('no move');
}
if (Game.restrictions.noZoom) {
restrictionsForDisplay.push('no zoom');
}
}
if (restrictionsForDisplay.length == 0) {
return;
}
// create restrictions span for header
var restrictions = document.createElement('span');
restrictions.setAttribute('id', 'restrictions');
restrictions.setAttribute('class', 'hideOnNarrowScreen');
var restrictionsTitle = document.createElement('span');
restrictionsTitle.setAttribute('class', 'bold');
restrictionsTitle.innerText = 'Restrictions: ';
var restrictionsList = document.createElement('span');
restrictionsList.innerText = restrictionsForDisplay.join(', ');
restrictions.appendChild(restrictionsTitle);
restrictions.appendChild(restrictionsList);
var roundContainer = document.getElementById('roundContainer');
var header = roundContainer.parentNode;
header.insertBefore(restrictions, roundContainer);
},
disableRestrictions: function () {
Game.panorama.setOptions({
clickToGo: true,
linksControl: true,
scrollwheel: true
});
document.getElementById('panningBlockerCover').style.display = null;
Game.startCountdown(0);
Game.timeoutEnd = null;
},
hideRestrictions: function () {
var restrictions = document.getElementById('restrictions');
if (restrictions) {
var header = restrictions.parentNode;
header.removeChild(restrictions);
}
},
transitToResultMap: function () {
// TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
document.getElementById('showGuessButton').click();
}
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
if (Game.guessMarker) {
Game.guessMarker.setMap(null);
Game.guessMarker = null;
}
document.getElementById('guess').classList.add('result');
Game.map.setOptions({
draggableCursor: 'grab'
});
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
} else if (Game.type == GameType.MULTI) {
if (Game.multi.owner) {
if (!Game.readyToContinue) {
document.getElementById('continueButton').disabled = true;
}
} else {
document.getElementById('continueButton').style.display = 'none';
}
}
},
loadHistory: function (response) {
if (!response.history)
return;
Game.history = response.history;
for (var i = 0; i < Game.rounds.length; ++i) {
var round = Game.rounds[i];
if (round.realMarker) {
round.realMarker.setMap(null);
}
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setMap(null);
guessMarker.line.setMap(null);
if (guessMarker.info) {
guessMarker.info.close();
}
}
}
Game.rounds = [];
Game.scoreSum = 0;
for (var i = 0; i < Game.history.length; ++i) {
var round = Game.history[i];
if (round.result) {
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true);
if (round.result.guessPosition) {
Game.addGuessPositionToResultMap(round.result.guessPosition, round.result, true);
}
Game.scoreSum += round.result.score;
if (round.allResults !== undefined) {
for (var j = 0; j < round.allResults.length; ++j) {
var result = round.allResults[j];
if (result.guessPosition) {
Game.addGuessPositionToResultMap(result.guessPosition, result, true);
}
}
}
}
}
},
reset: function () { reset: function () {
if (Game.guessMarker) { if (Game.guessMarker) {
Game.guessMarker.setMap(null); Game.guessMarker.setMap(null);
@@ -323,6 +518,7 @@
distanceInfo.children[0].style.display = null; distanceInfo.children[0].style.display = null;
distanceInfo.children[1].style.display = null; distanceInfo.children[1].style.display = null;
distanceInfo.children[2].style.display = null; distanceInfo.children[2].style.display = null;
document.getElementById('summaryInfo').innerHTML = "Game finished."
var scoreInfo = document.getElementById('scoreInfo'); var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = null; scoreInfo.children[0].style.display = null;
scoreInfo.children[1].style.display = null; scoreInfo.children[1].style.display = null;
@@ -334,6 +530,16 @@
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.disableRestrictions();
Game.hideRestrictions();
document.getElementById('panningBlockerCover').style.display = null;
Game.history = [];
Game.initialize(); Game.initialize();
}, },
@@ -358,7 +564,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 +588,12 @@
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)";
Game.enableRestrictions();
}, },
handleErrorResponse: function (error) { handleErrorResponse: function (error) {
@@ -398,6 +613,14 @@
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 was not found by this ID. Please check the link.');
break;
case 'anonymous_user':
MapGuesser.showModalWithContent('Error', 'You have to login to join a challenge!');
break;
default: default:
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\''); MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
break break
@@ -425,7 +648,7 @@
resultBounds.extend(position); resultBounds.extend(position);
if (guessPosition) { if (guessPosition) {
Game.addGuessPositionToResultMap(guessPosition); Game.addGuessPositionToResultMap(guessPosition, result);
resultBounds.extend(guessPosition); resultBounds.extend(guessPosition);
} }
@@ -461,25 +684,9 @@
}, },
showResultMap: function (result, resultBounds) { showResultMap: function (result, resultBounds) {
// TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
document.getElementById('showGuessButton').click();
}
if (Game.adaptGuess) { Game.transitToResultMap();
document.getElementById('guess').classList.remove('adapt');
}
if (Game.guessMarker) {
Game.guessMarker.setMap(null);
Game.guessMarker = null;
}
document.getElementById('guess').classList.add('result');
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds); Game.map.fitBounds(resultBounds);
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
@@ -498,38 +705,32 @@
var scoreBar = document.getElementById('scoreBar'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; scoreBar.style.width = scoreBarProperties.width;
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
} else if (roomId) {
if (Game.multi.owner) {
if (!Game.readyToContinue) {
document.getElementById('continueButton').disabled = true;
}
} else {
document.getElementById('continueButton').style.display = 'none';
}
}
}, },
guess: function () { guess: function () {
if (!Game.guessMarker) {
return; var data = new FormData();
if (Game.timeoutEnd) {
var timeLeft = Math.ceil((Game.timeoutEnd - new Date()) / 1000);
data.append('timeLeft', timeLeft);
} }
var guessPosition = Game.guessMarker.getPosition().toJSON(); Game.disableRestrictions();
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
if (Game.guessMarker) {
var guessPosition = Game.guessMarker.getPosition().toJSON();
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng));
}
document.getElementById('guessButton').disabled = true; document.getElementById('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible'; document.getElementById('panoCover').style.visibility = 'visible';
var data = new FormData();
data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng));
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json'; var url = Game.getGameIdentifier() + '/guess.json';
MapGuesser.httpRequest('POST', url, function () { MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
@@ -538,12 +739,16 @@
return; return;
} }
Game.loadHistory(this.response);
Game.restrictions = this.response.restrictions;
Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults); Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
if (this.response.place) { if (this.response.place) {
Game.panoId = this.response.place.panoId; Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov; Game.pov = this.response.place.pov;
} }
}, data); }, data);
}, },
@@ -577,8 +782,8 @@
var position = round.position; var position = round.position;
var guessMarker = { marker: null, line: null, info: null }; var guessMarker = { marker: null, line: null, info: null };
var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg'; var markerSvg = result && result.userName ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg';
var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?'; var markerLabel = result && result.userName ? result.userName.charAt(0).toUpperCase() : '?';
guessMarker.marker = new google.maps.Marker({ guessMarker.marker = new google.maps.Marker({
map: Game.map, map: Game.map,
@@ -628,8 +833,9 @@
}); });
if (result) { if (result) {
const userName = result.userName ? result.userName : 'me';
guessMarker.info = new google.maps.InfoWindow({ guessMarker.info = new google.maps.InfoWindow({
content: '<p class="small bold">' + result.userName + '</p>' + content: '<p class="small bold">' + userName + '</p>' +
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>', '<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
}); });
@@ -656,6 +862,36 @@
return { width: percent + '%', backgroundColor: color }; return { width: percent + '%', backgroundColor: color };
}, },
calculateHighScores: function () {
var highscores = new Map();
highscores.set('me', Game.scoreSum);
// collect the results of users who are through the last round
const round = Game.history[Game.history.length - 1];
if (round.allResults) {
for (const result of round.allResults) {
highscores.set(result.userName, result.score);
}
}
// add up scores only for the finishers
for (var i = Game.history.length - 2; i >= 0; --i) {
const round = Game.history[i];
if (round.allResults) {
for (const result of round.allResults) {
if (highscores.has(result.userName)) {
highscores.set(result.userName, highscores.get(result.userName) + result.score);
}
}
}
}
var sortedHighscores = Array.from(highscores, ([userName, score]) => ({ 'userName': userName, 'score': score }))
.sort(function (resultA, resultB) { return resultB.score - resultA.score });
return sortedHighscores;
},
showSummary: function () { showSummary: function () {
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = 'none'; distanceInfo.children[0].style.display = 'none';
@@ -666,11 +902,13 @@
scoreInfo.children[1].style.display = 'block'; scoreInfo.children[1].style.display = 'block';
document.getElementById('showSummaryButton').style.display = null; document.getElementById('showSummaryButton').style.display = null;
if (!roomId || Game.multi.owner) { if (Game.type == GameType.SINGLE || Game.multi.owner) {
document.getElementById('startNewGameButton').style.display = 'block'; document.getElementById('startNewGameButton').style.display = 'block';
if (!Game.readyToContinue) { if (!Game.readyToContinue) {
document.getElementById('startNewGameButton').disabled = true; document.getElementById('startNewGameButton').disabled = true;
} }
} else if (Game.type == GameType.CHALLENGE) {
document.getElementById('goToStart').style.display = 'block';
} }
var resultBounds = new google.maps.LatLngBounds(); var resultBounds = new google.maps.LatLngBounds();
@@ -713,6 +951,48 @@
var scoreBar = document.getElementById('scoreBar'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; scoreBar.style.width = scoreBarProperties.width;
Game.showHighscores();
},
showHighscores: function () {
if (Game.type == GameType.CHALLENGE) {
var highscores = this.calculateHighScores();
var summaryInfo = document.getElementById('summaryInfo');
if (highscores.length > 2) {
var table = document.getElementById('highscoresTable');
for (const result of highscores) {
var userName = document.createElement('td');
userName.innerHTML = result.userName;
var score = document.createElement('td');
score.innerHTML = result.score;
var line = document.createElement('tr');
line.appendChild(userName);
line.appendChild(score);
table.appendChild(line);
if (result.userName === 'me') {
line.setAttribute('class', 'ownPlayer');
}
}
MapGuesser.showModal('highscores');
} else if (highscores.length == 2) {
if (highscores[0].userName === 'me') {
summaryInfo.innerHTML = 'You won! <span class="hideOnNarrowScreen">' + highscores[1].userName + ' got only ' + highscores[1].score + ' points.</span>';
} else {
summaryInfo.innerHTML = 'You lost! <span class="hideOnNarrowScreen">' + highscores[0].userName + ' won with ' + highscores[0].score + ' points.</span>';
}
} else if (highscores.length == 1) {
summaryInfo.innerHTML = 'You are the first to finish. <span class="hideOnNarrowScreen">Invite your friends by sending them the link.</span>'
}
}
}, },
rewriteGoogleLink: function () { rewriteGoogleLink: function () {
@@ -735,7 +1015,7 @@
}, 1); }, 1);
}, },
startCountdown: function (timeout) { startCountdown: function (timeout, timedOutHandler) {
if (Game.countdownHandler) { if (Game.countdownHandler) {
clearInterval(Game.countdownHandler); clearInterval(Game.countdownHandler);
} }
@@ -757,7 +1037,12 @@
Game.setCountdownTime(timeLeft); Game.setCountdownTime(timeLeft);
if (timeLeft <= 0) { if (timeLeft <= 0) {
document.getElementById('panoCover').style.visibility = 'visible'; if (typeof timedOutHandler === 'function') {
timedOutHandler();
} else {
document.getElementById('panoCover').style.visibility = 'visible';
}
clearInterval(Game.countdownHandler); clearInterval(Game.countdownHandler);
} }
}, 1000); }, 1000);
@@ -855,8 +1140,17 @@
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 (roomId !== null) {
Game.type = GameType.MULTI;
} else if (challengeToken !== null) {
Game.type = GameType.CHALLENGE;
}
if (COOKIES_CONSENT) { if (COOKIES_CONSENT) {
Game.prepare(); Game.prepare();
} }
@@ -864,11 +1158,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 +1232,16 @@
} }
}); });
} }
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 });
}
document.getElementById('closeHighscoresButton').onclick = function () {
MapGuesser.hideModal();
};
})(); })();
+322 -81
View File
@@ -7,7 +7,6 @@
description: null description: null
}, },
map: null, map: null,
markers: null,
panorama: null, panorama: null,
selectedMarker: null, selectedMarker: null,
added: {}, added: {},
@@ -19,6 +18,7 @@
MapEditor.metadata.name = form.elements.name.value; MapEditor.metadata.name = form.elements.name.value;
MapEditor.metadata.description = form.elements.description.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]'; document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
@@ -87,7 +87,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 +118,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 +162,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 +220,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 +236,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);
@@ -257,6 +255,9 @@
if (MapEditor.metadata.description !== null) { if (MapEditor.metadata.description !== null) {
data.append('description', MapEditor.metadata.description); data.append('description', MapEditor.metadata.description);
} }
if (MapEditor.metadata.unlisted !== null) {
data.append('unlisted', MapEditor.metadata.unlisted);
}
for (var placeId in MapEditor.added) { for (var placeId in MapEditor.added) {
if (!MapEditor.added.hasOwnProperty(placeId)) { if (!MapEditor.added.hasOwnProperty(placeId)) {
@@ -310,42 +311,9 @@
var placeId = addedPlaces[i].id; var placeId = addedPlaces[i].id;
places[tempId].id = placeId; places[tempId].id = placeId;
} }
},
// 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: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
} }
}; };
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 = { var Util = {
getHighResData: function () { getHighResData: function () {
if (window.devicePixelRatio >= 4) { if (window.devicePixelRatio >= 4) {
@@ -384,52 +352,308 @@
} }
}; };
MapEditor.map = L.map('map', { var LMapWrapper = {
zoomControl: false 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) { init: function (divId, places) {
MapEditor.placeMarker(e.latlng); document.getElementById(divId).style.display = "block";
});
var highResData = Util.getHighResData(); if (!LMapWrapper.map) {
LMapWrapper.divId = divId;
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2
});
L.tileLayer(tileUrl, { LMapWrapper.map.on('click', function (e) {
attribution: tileAttribution, LMapWrapper.placeMarker(e.latlng);
subdomains: '1234', });
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(MapEditor.map);
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east })); var highResData = Util.getHighResData();
MapEditor.markers = L.markerClusterGroup({ L.tileLayer(tileUrl, {
maxClusterRadius: 50 attribution: tileAttribution,
}); subdomains: tileSubdomains,
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map);
for (var placeId in places) { if (mapId) {
if (!places.hasOwnProperty(placeId)) { LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
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 }, { init: function (divId, places) {
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen, document.getElementById(divId).style.display = "block";
zIndexOffset: 1000
}) if (!GMapWrapper.map) {
.addTo(MapEditor.markers) GMapWrapper.divId = divId;
.on('click', function () { 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); 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'), { 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
@@ -478,7 +702,7 @@
var coordinates = Util.extractCoordinates(coordinatesStr); var coordinates = Util.extractCoordinates(coordinatesStr);
if (coordinates.valid) { if (coordinates.valid) {
MapEditor.placeMarker(coordinates.latlng); MapEditor.map.placeMarker(coordinates.latlng);
} }
}; };
@@ -491,7 +715,7 @@
jumpButton.disabled = false; jumpButton.disabled = false;
if (e.key == 'Enter') { if (e.key == 'Enter') {
MapEditor.placeMarker(coordinates.latlng); MapEditor.map.placeMarker(coordinates.latlng);
} }
} }
else { else {
@@ -499,4 +723,21 @@
} }
}; };
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();
}
})(); })();
+69
View File
@@ -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.addStaticMaps();
Maps.initializeDescriptionDivs(); Maps.initializeDescriptionDivs();
@@ -85,10 +111,42 @@
window.location.href = '/multiGame/' + this.elements.roomId.value; 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 () { document.getElementById('multiButton').onclick = function () {
MapGuesser.showModal('multi'); MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId; document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select(); 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 () { document.getElementById('closePlayModeButton').onclick = function () {
@@ -99,6 +157,10 @@
MapGuesser.hideModal(); MapGuesser.hideModal();
}; };
document.getElementById('closeChallengeButton').onclick = function () {
MapGuesser.hideModal();
}
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton'); var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) { for (var i = 0; i < buttons.length; i++) {
var button = buttons[i]; var button = buttons[i];
@@ -107,6 +169,13 @@
MapGuesser.showModal('playMode'); MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId; document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = 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;
}
})(); })();
+3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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);
} }
+134 -1
View File
@@ -2,20 +2,29 @@
use DateTime; use DateTime;
use Faker\Factory; use Faker\Factory;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect; use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector; use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom; use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository; use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Response\Redirect; use MapGuesser\Response\Redirect;
class GameController class GameController implements ISecured
{ {
const NUMBER_OF_ROUNDS = 5;
private IRequest $request; private IRequest $request;
private PersistentDataManager $pdm; private PersistentDataManager $pdm;
@@ -26,6 +35,12 @@ class GameController
private MapRepository $mapRepository; private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
@@ -33,6 +48,14 @@ class GameController
$this->multiConnector = new MultiConnector(); $this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository(); $this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository(); $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 public function getGame(): IContent
@@ -79,6 +102,75 @@ class GameController
return new HtmlContent('game', ['roomId' => $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 public function prepareGame(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
@@ -114,6 +206,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);
@@ -149,6 +246,42 @@ class GameController
]); ]);
} }
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 private function getMultiToken(string $roomId): string
{ {
$session = $this->request->session(); $session = $this->request->session();
+239 -5
View File
@@ -1,16 +1,31 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent; 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\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\MultiRoomRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository; 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 NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000; const MAX_SCORE = 1000;
@@ -25,6 +40,20 @@ class GameFlowController
private PlaceRepository $placeRepository; 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) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
@@ -32,6 +61,18 @@ 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->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 authorize(): bool
{
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
} }
public function initialData(): IContent public function initialData(): IContent
@@ -110,6 +151,110 @@ class GameFlowController
return new JsonContent(['ok' => true]); 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 public function guess(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
@@ -121,7 +266,7 @@ class GameFlowController
$last = $state['rounds'][$state['currentRound']]; $last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']); $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$last['guessPosition'] = $guessPosition; $last['guessPosition'] = $guessPosition;
$last['distance'] = $result['distance']; $last['distance'] = $result['distance'];
@@ -145,9 +290,31 @@ class GameFlowController
$session->set('state', $state); $session->set('state', $state);
$this->saveVisit($last['placeId']);
return new JsonContent($response); return new JsonContent($response);
} }
// save the selected place for the round in UserPlayedPlace
private function saveVisit($placeId): void
{
$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 public function multiGuess(): IContent
{ {
$roomId = $this->request->query('roomId'); $roomId = $this->request->query('roomId');
@@ -162,7 +329,7 @@ class GameFlowController
$last = $state['rounds'][$state['currentRound']]; $last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']); $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [ $responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId, 'roomId' => $roomId,
@@ -185,6 +352,70 @@ class GameFlowController
return new JsonContent($response); 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 public function multiNextRound(): IContent
{ {
$roomId = $this->request->query('roomId'); $roomId = $this->request->query('roomId');
@@ -214,7 +445,7 @@ class GameFlowController
return new JsonContent(['ok' => true]); return new JsonContent(['ok' => true]);
} }
private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area) private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area)
{ {
$distance = $this->calculateDistance($realPosition, $guessPosition); $distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area); $score = $this->calculateScore($distance, $area);
@@ -224,7 +455,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;
+33
View File
@@ -14,10 +14,12 @@ 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;
use MapGuesser\Response\Redirect; use MapGuesser\Response\Redirect;
use MapGuesser\Util\CaptchaValidator;
use MapGuesser\Util\JwtParser; use MapGuesser\Util\JwtParser;
class LoginController class LoginController
@@ -32,6 +34,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 +43,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()
@@ -281,6 +286,18 @@ class LoginController
return new JsonContent($data); 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) { if (filter_var($this->request->post('email'), FILTER_VALIDATE_EMAIL) === false) {
return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]); return new JsonContent(['error' => ['errorText' => 'The given email address is not valid.']]);
} }
@@ -430,6 +447,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();
@@ -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')); $user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user === null) { if ($user === null) {
+58 -2
View File
@@ -5,11 +5,18 @@ use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
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 +34,27 @@ class MapAdminController implements ISecured
private PlaceRepository $placeRepository; private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private ChallengeRepository $challengeRepository;
private GuessRepository $guessRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
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();
$this->challengeRepository = new ChallengeRepository();
$this->guessRepository = new GuessRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
} }
public function authorize(): bool public function authorize(): bool
@@ -59,6 +81,7 @@ class MapAdminController implements ISecured
'mapId' => $mapId, 'mapId' => $mapId,
'mapName' => $map->getName(), 'mapName' => $map->getName(),
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
'mapUnlisted' => $map->getUnlisted(),
'bounds' => $map->getBounds()->toArray(), 'bounds' => $map->getBounds()->toArray(),
'places' => &$places 'places' => &$places
]); ]);
@@ -138,7 +161,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);
} }
} }
@@ -153,6 +176,9 @@ class MapAdminController implements ISecured
if (isset($_POST['description'])) { if (isset($_POST['description'])) {
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_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); $this->pdm->saveToDb($map);
@@ -178,13 +204,43 @@ 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);
}
foreach ($this->challengeRepository->getAllByPlace($place) as $challenge) {
$this->deleteChallenge($challenge);
}
$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);
} }
} }
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 private function calculateMapBounds(Map $map): Bounds
{ {
$bounds = new Bounds(); $bounds = new Bounds();
+9 -2
View File
@@ -30,12 +30,19 @@ class MapsController
['maps', 'bound_north_lat'], ['maps', 'bound_north_lat'],
['maps', 'bound_east_lng'], ['maps', 'bound_east_lng'],
['maps', 'area'], ['maps', 'area'],
['maps', 'unlisted'],
new RawExpression('COUNT(places.id) AS num_places') new RawExpression('COUNT(places.id) AS num_places')
]); ]);
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']); $select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
$select->groupBy(['maps', 'id']); $select->groupBy(['maps', 'id']);
$select->orderBy('name'); $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(); $result = $select->execute();
$maps = []; $maps = [];
@@ -45,10 +52,10 @@ class MapsController
$maps[] = $map; $maps[] = $map;
} }
$user = $this->request->user();
return new HtmlContent('maps', [ return new HtmlContent('maps', [
'maps' => $maps, 'maps' => $maps,
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN) 'isLoggedIn' => $user !== null,
'isAdmin' => $isAdmin
]); ]);
} }
+26 -1
View File
@@ -9,8 +9,12 @@ use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\OAuth\GoogleOAuth; use MapGuesser\OAuth\GoogleOAuth;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserInChallengeRepository;
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 +30,21 @@ class UserController implements ISecured
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private UserInChallengeRepository $userInChallengeRepository;
private GuessRepository $guessRepository;
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();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->guessRepository = new GuessRepository();
} }
public function authorize(): bool public function authorize(): bool
@@ -201,6 +214,18 @@ class UserController implements ISecured
$this->pdm->deleteFromDb($userPasswordResetter); $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); $this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit(); \Container::$dbConnection->commit();
@@ -210,7 +235,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;
+47 -14
View File
@@ -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];
}
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) { 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
+112
View File
@@ -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');
}
}
+133
View File
@@ -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;
}
}
+13 -1
View File
@@ -6,7 +6,7 @@ class Map extends Model
{ {
protected static string $table = 'maps'; 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 = ''; private string $name = '';
@@ -16,6 +16,8 @@ class Map extends Model
private float $area = 0.0; private float $area = 0.0;
private bool $unlisted = false;
public function __construct() public function __construct()
{ {
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0); $this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
@@ -61,6 +63,11 @@ class Map extends Model
$this->area = $area; $this->area = $area;
} }
public function setUnlisted(bool $unlisted): void
{
$this->unlisted = $unlisted;
}
public function getName(): string public function getName(): string
{ {
return $this->name; return $this->name;
@@ -100,4 +107,9 @@ class Map extends Model
{ {
return $this->area; return $this->area;
} }
public function getUnlisted(): bool
{
return $this->unlisted;
}
} }
+2 -8
View File
@@ -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,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;
}
}
@@ -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;
}
}
+72 -63
View File
@@ -8,9 +8,9 @@ use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager class PersistentDataManager
{ {
public function selectFromDb(Select $select, string $type, bool $withRelations = false) 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); $data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
@@ -19,50 +19,75 @@ class PersistentDataManager
} }
$model = new $type(); $model = new $type();
$this->fillWithData($data, $model); $this->fillWithData($data, $model, $withRelations);
return $model; 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(); $result = $select->execute();
while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) { while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) {
$model = new $type(); $model = new $type();
$this->fillWithData($data, $model); $this->fillWithData($data, $model, $withRelations);
yield $model; yield $model;
} }
} }
public function selectFromDbById($id, string $type, bool $withRelations = false) public function selectFromDbById($id, string $type, bool $useRelations = false)
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
$select->whereId($id); $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(); $relations = $model::getRelations();
$relationData = []; if (count($withRelations)) {
$relations = array_intersect($relations, $withRelations);
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);
}
} }
$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(); $model->saveSnapshot();
} }
@@ -128,35 +153,37 @@ class PersistentDataManager
$model->resetSnapshot(); $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']); $table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']); $fields = call_user_func([$type, 'getFields']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$select->from($table); $select->from($table);
//TODO: only with some relations? if ($useRelations) {
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']); $relations = call_user_func([$type, 'getRelations']);
if (count($withRelations)) {
$columns = []; $relations = array_intersect($relations, $withRelations);
foreach ($fields as $field) {
$columns[] = [$table, $field];
} }
$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); $select->columns($columns);
} else { } else {
$select->columns($fields); $select->columns($columns);
} }
return $select; return $select;
} }
private function getRelationColumns(array $relations): array private function getRelationColumns(array $relations, array $withRelations): array
{ {
$columns = []; $columns = [];
@@ -165,46 +192,28 @@ class PersistentDataManager
foreach (call_user_func([$relationType, 'getFields']) as $relationField) { foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
$columns[] = [$relationTable, $relationField, $relation . '__' . $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; 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) { foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']); $relationTable = call_user_func([$relationType, 'getTable']);
$select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']); $select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']);
}
}
private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool $nextOrderRelations = call_user_func([$relationType, 'getRelations']);
{ if (count($withRelations)) {
$found = false; $nextOrderRelations = array_intersect($nextOrderRelations, $withRelations);
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);
} }
$this->leftJoinRelations($select, $relationTable, $nextOrderRelations, $withRelations);
} }
} }
+75
View File
@@ -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);
}
}
+98
View File
@@ -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);
}
}
+19
View File
@@ -1,6 +1,9 @@
<?php namespace MapGuesser\Repository; <?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
class MapRepository class MapRepository
@@ -16,4 +19,20 @@ class MapRepository
{ {
return $this->pdm->selectFromDbById($mapId, Map::class); 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,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);
}
}
+138 -18
View File
@@ -2,6 +2,7 @@
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
@@ -28,13 +29,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 +66,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 +85,118 @@ 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;
}
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);
}
}
+6
View File
@@ -3,6 +3,7 @@
use DateTime; use DateTime;
use Generator; use Generator;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
@@ -44,4 +45,9 @@ class UserRepository
yield from $this->pdm->selectMultipleFromDb($select, User::class); yield from $this->pdm->selectMultipleFromDb($select, User::class);
} }
public function getByGuess(Guess $guess): ?User
{
return $this->getById($guess->getUserId());
}
} }
+19
View File
@@ -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);
}
}
+6 -6
View File
@@ -9,24 +9,24 @@
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?> <?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> <p class="justify small">Please confirm your identity with your password or with Google to modify your account.</p>
<div class="inputWithButton"> <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> --><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div> </div>
<?php elseif ($user['password'] !== null): ?> <?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to modify your account.</p> <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): ?> <?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to modify your account.</p> <p class="justify small">Please confirm your identity with Google to modify your account.</p>
<div class="inputWithButton"> <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> --><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div> </div>
<?php endif; ?> <?php endif; ?>
<hr> <hr>
<?php /* TODO: disabled for the time being, email modification should be implemented */ ?> <?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 type="email" class="text big fullWidth" 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 type="password" class="text big fullWidth marginTop" 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="password" class="text big fullWidth marginTop" name="password_new_confirm" placeholder="New password confirmation" minlength="6">
<p id="accountFormError" class="formError justify marginTop"></p> <p id="accountFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit" name="submit" disabled>Save</button> <button type="submit" name="submit" disabled>Save</button>
+3 -3
View File
@@ -10,16 +10,16 @@
<?php if ($user['password'] !== null && $user['google_sub'] !== null): ?> <?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> <p class="justify small">Please confirm your identity with your password or with Google to delete your account.</p>
<div class="inputWithButton"> <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> --><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div> </div>
<?php elseif ($user['password'] !== null): ?> <?php elseif ($user['password'] !== null): ?>
<p class="justify small">Please confirm your identity with your password to delete your account.</p> <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): ?> <?php elseif ($user['google_sub'] !== null): ?>
<p class="justify small">Please confirm your identity with Google to delete your account.</p> <p class="justify small">Please confirm your identity with Google to delete your account.</p>
<div class="inputWithButton"> <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> --><button id="authenticateWithGoogleButton" class="yellow" type="button">Google</button>
</div> </div>
<?php endif; ?> <?php endif; ?>
+28 -5
View File
@@ -6,14 +6,20 @@
@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> --><span class="inline hideOnMobile" id="mapSelection">
<input type="text" id="jumpCoordinates" placeholder="Insert coordinates here" /> <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> <button id="jumpButton" disabled >Jump</button>
</span><!-- </span><!--
--><span><!-- --><span><!--
@@ -46,8 +52,11 @@
<div id="metadata" class="modal"> <div id="metadata" class="modal">
<h2>Edit map data</h2> <h2>Edit map data</h2>
<form id="metadataForm" class="marginTop" data-no-submit="true"> <form id="metadataForm" class="marginTop" data-no-submit="true">
<input class="fullWidth" type="text" name="name" value="<?= $mapName ?>" placeholder="Name of the map"> <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> <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"> <div class="right">
<button class="marginTop marginRight" type="submit">Apply</button><!-- <button class="marginTop marginRight" type="submit">Apply</button><!--
--><button id="closeMetadataButton" class="gray marginTop" type="button">Close</button> --><button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
@@ -57,7 +66,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>
@@ -75,6 +97,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) ?>;
+37 -2
View File
@@ -12,11 +12,25 @@
<div id="players" class="marginTop"></div> <div id="players" class="marginTop"></div>
<button id="startMultiGameButton" class="button fullWidth marginTop green">Start game</button> <button id="startMultiGameButton" class="button fullWidth marginTop green">Start game</button>
</div> </div>
<div id="highscores" class="modal">
<h2>Highscores</h2>
<div>
<table id="highscoresTable">
<tr>
<th>Player</th>
<th>Score</th>
</tr>
</table>
</div>
<div class="right">
<button id="closeHighscoresButton" class="gray marginTop" type="button">Close</button>
</div>
</div>
@endsection @endsection
@section(subheader) @section(subheader)
<span id="mapName" class="bold">Loading map...</span><!-- <span id="mapName" class="bold">Loading map...</span><!--
--><span>Round <span id="currentRound" class="bold"></span></span><!-- --><span id="roundContainer">Round <span id="currentRound" class="bold"></span></span><!--
--><span>Score <span id="currentScoreSum" class="bold"></span></span> --><span>Score <span id="currentScoreSum" class="bold"></span></span>
@endsection @endsection
@@ -25,6 +39,7 @@
<p id="countdownTime" class="mono bold"></p> <p id="countdownTime" class="mono bold"></p>
</div> </div>
<div id="panoCover"></div> <div id="panoCover"></div>
<div id="panningBlockerCover"></div>
<div id="panorama"></div> <div id="panorama"></div>
<div id="showGuessButtonContainer"> <div id="showGuessButtonContainer">
<button id="showGuessButton" class="fullWidth">Show guess map</button> <button id="showGuessButton" class="fullWidth">Show guess map</button>
@@ -41,7 +56,7 @@
<div id="distanceInfo"> <div id="distanceInfo">
<p>You were <span id="distance" class="bold"></span> close.</p> <p>You were <span id="distance" class="bold"></span> close.</p>
<p>You didn't guess in this round.</p> <p>You didn't guess in this round.</p>
<p class="bold">Game finished.</p> <p id="summaryInfo" class="bold">Game finished.</p>
</div> </div>
<div id="scoreInfo"> <div id="scoreInfo">
<p>You earned <span id="score" class="bold"></span> points.</p> <p>You earned <span id="score" class="bold"></span> points.</p>
@@ -57,6 +72,25 @@
<button id="continueButton" class="fullWidth">Continue</button> <button id="continueButton" class="fullWidth">Continue</button>
<button id="showSummaryButton" class="fullWidth">Show summary</button> <button id="showSummaryButton" class="fullWidth">Show summary</button>
<button id="startNewGameButton" class="fullWidth">Play this map again</button> <button id="startNewGameButton" class="fullWidth">Play this map again</button>
<a href="/" id="goToStart"><button class="fullWidth">Go back to the menu</button></a>
</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>
</div> </div>
@endsection @endsection
@@ -66,5 +100,6 @@
var multiUrl = '<?= $_ENV['MULTI_WS_URL'] ?>'; var multiUrl = '<?= $_ENV['MULTI_WS_URL'] ?>';
var roomId = <?= isset($roomId) ? '\'' . $roomId . '\'' : 'null' ?>; var roomId = <?= isset($roomId) ? '\'' . $roomId . '\'' : 'null' ?>;
var mapId = <?= isset($mapId) ? '\'' . $mapId . '\'' : 'null' ?>; var mapId = <?= isset($mapId) ? '\'' . $mapId . '\'' : 'null' ?>;
var challengeToken = <?= isset($challengeToken) ? '\'' . $challengeToken . '\'' : 'null' ?>;
</script> </script>
@endsection @endsection
+1 -1
View File
@@ -11,7 +11,7 @@
<?php else: ?> <?php else: ?>
<p class="justify">Please confirm your sign up request. Your account will be linked to your Google account.</p> <p class="justify">Please confirm your sign up request. Your account will be linked to your Google account.</p>
<?php endif; ?> <?php endif; ?>
<input class="big fullWidth marginTop" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled> <input type="email" class="text big fullWidth marginTop" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<div class="right"> <div class="right">
<button class="marginTop marginRight" type="submit"> <button class="marginTop marginRight" type="submit">
<?php if ($found): ?> <?php if ($found): ?>
+2 -2
View File
@@ -4,8 +4,8 @@
<h2>Login</h2> <h2>Login</h2>
<div class="box"> <div class="box">
<form id="loginForm" action="/login" method="post" data-redirect-on-success="/"> <form id="loginForm" action="/login" method="post" data-redirect-on-success="/">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" required autofocus> <input type="email" class="text big fullWidth" name="email" placeholder="Email address" required autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6"> <input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" required minlength="6">
<p id="loginFormError" class="formError justify marginTop"></p> <p id="loginFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit">Login</button> <button type="submit">Login</button>
+8 -1
View File
@@ -1,10 +1,17 @@
@js(https://www.google.com/recaptcha/api.js)
@extends(templates/layout_normal) @extends(templates/layout_normal)
@section(main) @section(main)
<h2>Request password reset</h2> <h2>Request password reset</h2>
<div class="box"> <div class="box">
<form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success"> <form id="passwordResetForm" action="/password/requestReset" method="post" data-redirect-on-success="/password/requestReset/success">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= isset($email) ? $email : '' ?>" required autofocus> <input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= isset($email) ? $email : '' ?>" required autofocus>
<?php if (!empty($_ENV['RECAPTCHA_SITEKEY'])): ?>
<div class="marginTop">
<div class="g-recaptcha" data-sitekey="<?= $_ENV['RECAPTCHA_SITEKEY'] ?>"></div>
</div>
<?php endif; ?>
<p id="passwordResetFormError" class="formError justify marginTop"></p> <p id="passwordResetFormError" class="formError justify marginTop"></p>
<div class="right marginTop"> <div class="right marginTop">
<button type="submit">Continue</button> <button type="submit">Continue</button>
+3 -3
View File
@@ -5,9 +5,9 @@
<div class="box"> <div class="box">
<?php if ($success) : ?> <?php if ($success) : ?>
<form id="resetPasswordForm" action="/password/reset/<?= $token ?>" method="post" data-redirect-on-success="/"> <form id="resetPasswordForm" action="/password/reset/<?= $token ?>" method="post" data-redirect-on-success="/">
<input class="big fullWidth" type="email" name="email" placeholder="Email address" value="<?= $email ?>" disabled> <input type="email" class="text big fullWidth" name="email" placeholder="Email address" value="<?= $email ?>" disabled>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6" autofocus> <input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" required minlength="6" autofocus>
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" required minlength="6"> <input type="password" class="text big fullWidth marginTop" name="password_confirm" placeholder="Password confirmation" required minlength="6">
<p id="resetPasswordFormError" class="formError justify marginTop"></p> <p id="resetPasswordFormError" class="formError justify marginTop"></p>
<div class="right"> <div class="right">
<button class="marginTop" type="submit">Reset password</button> <button class="marginTop" type="submit">Reset password</button>
+11 -5
View File
@@ -1,3 +1,4 @@
@js(https://www.google.com/recaptcha/api.js)
@js(js/login/signup.js) @js(js/login/signup.js)
@extends(templates/layout_normal) @extends(templates/layout_normal)
@@ -8,12 +9,17 @@
<form id="signupForm" action="/signup" method="post" data-redirect-on-success="/signup/success"> <form id="signupForm" action="/signup" method="post" data-redirect-on-success="/signup/success">
<?php if (isset($email)): ?> <?php if (isset($email)): ?>
<p class="justify">No user found with the given email address. Sign up with one click!</p> <p class="justify">No user found with the given email address. Sign up with one click!</p>
<input class="big fullWidth marginTop" type="email" name="email" placeholder="Email address" value="<?= $email ?>" required> <input type="email" class="text big fullWidth marginTop" name="email" placeholder="Email address" value="<?= $email ?>" required>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password confirmation" required minlength="6" autofocus> <input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password confirmation" required minlength="6" autofocus>
<?php else: ?> <?php else: ?>
<input class="big fullWidth" type="email" name="email" placeholder="Email address" required autofocus> <input type="email" class="text big fullWidth" name="email" placeholder="Email address" required autofocus>
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password" required minlength="6"> <input type="password" class="text big fullWidth marginTop" name="password" placeholder="Password" required minlength="6">
<input class="big fullWidth marginTop" type="password" name="password_confirm" placeholder="Password confirmation" minlength="6"> <input type="password" class="text big fullWidth marginTop" name="password_confirm" placeholder="Password confirmation" minlength="6">
<?php endif; ?>
<?php if (!empty($_ENV['RECAPTCHA_SITEKEY'])): ?>
<div class="marginTop">
<div class="g-recaptcha" data-sitekey="<?= $_ENV['RECAPTCHA_SITEKEY'] ?>"></div>
</div>
<?php endif; ?> <?php endif; ?>
<p id="signupFormError" class="formError justify marginTop"></p> <p id="signupFormError" class="formError justify marginTop"></p>
<div class="right"> <div class="right">
+45 -7
View File
@@ -11,6 +11,10 @@ TODO: condition!
<a id="singleButton" class="button fullWidth marginTop" href="" title="Single player">Single player</a> <a id="singleButton" class="button fullWidth marginTop" href="" title="Single player">Single player</a>
<p class="bold center marginTop marginBottom">OR</p> <p class="bold center marginTop marginBottom">OR</p>
<button id="multiButton" class="fullWidth green" data-map-id="">Multiplayer (beta)</button> <button id="multiButton" class="fullWidth green" data-map-id="">Multiplayer (beta)</button>
<?php if ($isLoggedIn): ?>
<p class="bold center marginTop marginBottom">OR</p>
<button id="challengeButton" class="fullWidth yellow" data-map-id="" data-timer="">Challenge (gamma)</button>
<?php endif; ?>
<div class="right"> <div class="right">
<button id="closePlayModeButton" class="gray marginTop" type="button">Close</button> <button id="closePlayModeButton" class="gray marginTop" type="button">Close</button>
</div> </div>
@@ -21,7 +25,7 @@ TODO: condition!
<a id="createNewRoomButton" class="button fullWidth green" href="" title="Create new room">Create new room</a> <a id="createNewRoomButton" class="button fullWidth green" href="" title="Create new room">Create new room</a>
<p class="bold center marginTop marginBottom">OR</p> <p class="bold center marginTop marginBottom">OR</p>
<div class="inputWithButton"> <div class="inputWithButton">
<input type="text" name="roomId" placeholder="Room to connect" required minlength="6" maxlength="6"><!-- <input type="text" class="text" name="roomId" placeholder="Room to connect" required minlength="6" maxlength="6"><!--
--><button id="authenticateWithGoogleButton" type="submit">Connect</button> --><button id="authenticateWithGoogleButton" type="submit">Connect</button>
</div> </div>
<div class="right"> <div class="right">
@@ -29,12 +33,42 @@ TODO: condition!
</div> </div>
</form> </form>
</div> </div>
<div id="challenge" class="modal">
<h2>Challenge (gamma)</h2>
<form id="challengeForm" class="marginTop" method="get">
<div id="restrictions" class="marginTop marginBottom">
<h3>Optional restrictions</h3>
<div class="marginTop">
<input type="checkbox" id="timerEnabled" name="timerEnabled" value="timerEnabled" /><label id="timeLimitLabel" for="timerEnabled">Time limit measured in seconds</label>
</div>
<div id="timeLimitType">
<input class="fullWidth" type="range" id="timeLimit" name="timeLimit" min="10" max="1800" step="10" value="300" />
<input type="radio" id="timeLimitTypeGame" name="timeLimitType" value="game" checked /><label for="timeLimitTypeGame">for the whole game</label>
<input class="marginLeft" type="radio" id="timeLimitTypeRound" name="timeLimitType" value="round" /><label for="timeLimitTypeRound">per round</label>
</div>
<div class="marginTop">
<input type="checkbox" id="noMove" name="noMove" value="noMove" /><label for="noMove">No movement allowed</label>
</div>
<div class="marginTop">
<input type="checkbox" id="noZoom" name="noZoom" value="noZoom" /><label for="noZoom">No zoom allowed</label>
</div>
<div class="marginTop">
<input type="checkbox" id="noPan" name="noPan" value="noPan" /><label for="noPan">No camera change allowed</label>
</div>
<input type="hidden" name="mapId" id="challengeMapId" />
</div>
<button id="createNewChallengeButton" type="submit" class="button fullWidth green" href="" title="Create new challenge">Create new challenge</button>
<div class="right">
<button id="closeChallengeButton" class="gray marginTop" type="button">Close</button>
</div>
</form>
</div>
@endsection @endsection
@section(main) @section(main)
<div id="mapContainer"> <div id="mapContainer">
<?php foreach ($maps as $map): ?> <?php foreach ($maps as $map): ?>
<div class="mapItem"> <div class="mapItem <?= $map['unlisted'] ? 'unlisted' : '' ?>">
<div class="title"> <div class="title">
<p class="title"><?= $map['name'] ?></p> <p class="title"><?= $map['name'] ?></p>
</div> </div>
@@ -62,12 +96,16 @@ TODO: condition!
</div> </div>
</div> </div>
<div class="buttonContainer"> <div class="buttonContainer">
<?php if ($isAdmin): ?> <?php if (!empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || Container::$request->user()): ?>
<button class="button fullWidth noRightRadius playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button> <?php if ($isAdmin): ?>
<a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a> <button class="button fullWidth noRightRadius playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
<button class="button red fullWidth noLeftRadius deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Delete map '<?= $map['name'] ?>'">Delete</button> <a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a>
<button class="button red fullWidth noLeftRadius deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Delete map '<?= $map['name'] ?>'">Delete</button>
<?php else: ?>
<button class="button fullWidth playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
<?php endif; ?>
<?php else: ?> <?php else: ?>
<button class="button fullWidth playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button> <a href="/login" class="button fullWidth" title="Play map '<?= $map['name'] ?>'">Play this map</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
+19 -2
View File
@@ -62,6 +62,13 @@ Container::$routeCollection->group('multiGame', function (MapGuesser\Routing\Rou
$routeCollection->post('multiGame.nextRound-json', '{roomId}/nextRound.json', [MapGuesser\Controller\GameFlowController::class, 'multiNextRound']); $routeCollection->post('multiGame.nextRound-json', '{roomId}/nextRound.json', [MapGuesser\Controller\GameFlowController::class, 'multiNextRound']);
$routeCollection->post('multiGame.guess-json', '{roomId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'multiGuess']); $routeCollection->post('multiGame.guess-json', '{roomId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'multiGuess']);
}); });
Container::$routeCollection->group('challenge', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->post('challenge.create', 'create.json', [\MapGuesser\Controller\GameController::class, 'createNewChallenge']);
$routeCollection->get('challenge', '{challengeToken}', [MapGuesser\Controller\GameController::class, 'getChallenge']);
$routeCollection->post('challenge.prepare-json', '{challengeToken}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareChallenge']);
$routeCollection->post('challenge.initialData-json', '{challengeToken}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'challengeInitialData']);
$routeCollection->post('challenge.guess-json', '{challengeToken}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'challengeGuess']);
});
Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']); $routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);
$routeCollection->get('admin.place', 'place.json/{placeId}', [MapGuesser\Controller\MapAdminController::class, 'getPlace']); $routeCollection->get('admin.place', 'place.json/{placeId}', [MapGuesser\Controller\MapAdminController::class, 'getPlace']);
@@ -74,13 +81,23 @@ if (isset($_COOKIE['COOKIES_CONSENT'])) {
session_set_save_handler(Container::$sessionHandler, true); session_set_save_handler(Container::$sessionHandler, true);
session_start([ session_start([
'gc_maxlifetime' => 604800,
'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand 'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand
'cookie_lifetime' => 604800, // TODO: cookie is not renewed so session can be lost 'cookie_lifetime' => 604800,
'cookie_path' => '/',
'cookie_httponly' => true, 'cookie_httponly' => true,
'cookie_samesite' => 'Lax' 'cookie_samesite' => 'Lax'
]); ]);
if (isset($_COOKIE[session_name()])) {
// extend session cookie lifetime is cookie already exists
setcookie(session_name(), session_id(), [
'expires' => time() + 604800,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax'
]);
}
// this is needed to handle old type of session IDs // this is needed to handle old type of session IDs
if (!Container::$sessionHandler->validateId(session_id())) { if (!Container::$sessionHandler->validateId(session_id())) {
session_regenerate_id(true); session_regenerate_id(true);