Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ca97dccb9 | ||
|
|
2ef67d53f4 | ||
|
|
06a0722899 | ||
|
|
4a9cf8a217 | ||
|
|
b3caf1fb38 | ||
|
|
08d4aec48e | ||
|
|
9c380076fa | ||
|
|
95508ad284 | ||
|
|
7690714b26 | ||
|
|
f732dad1b1 | ||
|
|
49b6f28a9d | ||
|
|
33ec9d3a9c | ||
|
|
6fb122e842 | ||
|
|
90ccdf6de7 | ||
|
|
a869a55b7e | ||
|
|
420ed400a6 | ||
|
|
d6f34b916e | ||
|
|
98fc735622 | ||
|
|
81a3079a1a | ||
|
|
a0d1e8778c | ||
|
|
2fc0ab557f | ||
|
|
be8f0a1486 | ||
|
|
ff201c2e8e | ||
|
|
805d91c0c0 | ||
|
|
ec7e806277 | ||
|
|
947ce6b183 | ||
|
|
6fc83c9459 | ||
|
|
5237c3f454 | ||
|
|
5a1c384c17 | ||
|
|
3fb538469b | ||
|
|
04d10663dd | ||
|
|
f11cba4f10 | ||
|
|
cfb0513825 | ||
|
|
ed0e287c94 | ||
|
|
ad818af543 | ||
|
|
2725754504 | ||
|
|
dc072bd3c0 | ||
|
|
a70d59ed95 | ||
|
|
94cab9f433 | ||
|
|
4b0057e3ba | ||
|
|
5a450c46db | ||
|
|
4af6f07ce1 | ||
|
|
5e28fcb64d | ||
|
|
16bcbaecbe | ||
|
|
fbab712496 | ||
|
|
46ef7f3b3b | ||
|
|
5dd877d075 | ||
|
|
88e8c5c027 | ||
|
|
3fd37b0a78 | ||
|
|
92fd282184 | ||
|
|
55a1cd24d6 | ||
|
|
bb29bfdfe1 | ||
|
|
1179914f42 | ||
|
|
85773d47a0 | ||
|
|
199374a851 | ||
|
|
138ff3fe6a | ||
|
|
1915b252c6 | ||
|
|
2f2a7968a0 | ||
|
|
edc2e4111a | ||
|
|
d93a758cd2 | ||
|
|
e8f9d74283 | ||
|
|
00dc78b50c | ||
|
|
fe814908c7 | ||
|
|
ecaf8ca9d4 | ||
|
|
ed9621d581 | ||
|
|
b6f8735da8 | ||
|
|
6a9f7eddc1 | ||
|
|
af52ac3787 | ||
|
|
0481274335 | ||
|
|
618f2a4d48 | ||
|
|
e817a34a3a | ||
|
|
4dde24b70a | ||
|
|
2d91f94f0c | ||
|
|
dd2905286b | ||
|
|
12d24bfddf | ||
|
|
338537c774 | ||
|
|
b2ee25fcc5 | ||
|
|
af2375133a | ||
|
|
83a027c1bb | ||
|
|
4b2fc23129 | ||
|
|
c0c44bcbae | ||
|
|
e0273a4c41 | ||
|
|
b2c1749695 | ||
|
|
425d0d891a | ||
|
|
0d3203c10e | ||
|
|
cf63287ecc | ||
|
|
8b9dd0bc32 | ||
|
|
6acb19ba36 | ||
|
|
7f8e0fefa9 | ||
|
|
87ef0d1918 | ||
|
|
41b989d6e3 |
No files matched your search
@@ -6,3 +6,5 @@ DB_NAME=mapguesser
|
||||
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
|
||||
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
|
||||
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
|
||||
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
|
||||
STATIC_ROOT=/static
|
||||
+2
-1
@@ -4,7 +4,8 @@
|
||||
"description": "MapGuesser Application",
|
||||
"license": "GNU GPL 3.0",
|
||||
"require": {
|
||||
"vlucas/phpdotenv": "^4.1"
|
||||
"vlucas/phpdotenv": "^4.1",
|
||||
"symfony/console": "^5.1"
|
||||
},
|
||||
"require-dev": {},
|
||||
"autoload": {
|
||||
|
||||
Generated
+672
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "29431cf83ee884f01ee954b1068c0ccc",
|
||||
"content-hash": "13a0eaff2786786caff2be86ac704fc7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
@@ -61,6 +61,148 @@
|
||||
],
|
||||
"time": "2020-03-21T18:07:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/container",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/container.git",
|
||||
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
|
||||
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Container\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common Container Interface (PHP FIG PSR-11)",
|
||||
"homepage": "https://github.com/php-fig/container",
|
||||
"keywords": [
|
||||
"PSR-11",
|
||||
"container",
|
||||
"container-interface",
|
||||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"time": "2017-02-14T16:28:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "00bed125812716d09b163f0727ef33bb49bf3448"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/00bed125812716d09b163f0727ef33bb49bf3448",
|
||||
"reference": "00bed125812716d09b163f0727ef33bb49bf3448",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/polyfill-php73": "^1.8",
|
||||
"symfony/polyfill-php80": "^1.15",
|
||||
"symfony/service-contracts": "^1.1|^2",
|
||||
"symfony/string": "^5.1"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<4.4",
|
||||
"symfony/dotenv": "<5.1",
|
||||
"symfony/event-dispatcher": "<4.4",
|
||||
"symfony/lock": "<4.4",
|
||||
"symfony/process": "<4.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "~1.0",
|
||||
"symfony/config": "^4.4|^5.0",
|
||||
"symfony/dependency-injection": "^4.4|^5.0",
|
||||
"symfony/event-dispatcher": "^4.4|^5.0",
|
||||
"symfony/lock": "^4.4|^5.0",
|
||||
"symfony/process": "^4.4|^5.0",
|
||||
"symfony/var-dumper": "^4.4|^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "For using the console logger",
|
||||
"symfony/event-dispatcher": "",
|
||||
"symfony/lock": "",
|
||||
"symfony/process": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony Console Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-30T20:35:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.17.0",
|
||||
@@ -133,6 +275,535 @@
|
||||
],
|
||||
"time": "2020-05-12T16:14:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "e094b0770f7833fdf257e6ba4775be4e258230b2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e094b0770f7833fdf257e6ba4775be4e258230b2",
|
||||
"reference": "e094b0770f7833fdf257e6ba4775be4e258230b2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's grapheme_* functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"grapheme",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "1357b1d168eb7f68ad6a134838e46b0b159444a9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/1357b1d168eb7f68ad6a134838e46b0b159444a9",
|
||||
"reference": "1357b1d168eb7f68ad6a134838e46b0b159444a9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's Normalizer class and related functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"intl",
|
||||
"normalizer",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-12T16:14:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||
"reference": "fa79b11539418b02fc5e1897267673ba2c19419c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Mbstring\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"mbstring",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php73",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php73.git",
|
||||
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||
"reference": "a760d8964ff79ab9bf057613a5808284ec852ccc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php73\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "5e30b2799bc1ad68f7feb62b60a73743589438dd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/5e30b2799bc1ad68f7feb62b60a73743589438dd",
|
||||
"reference": "5e30b2799bc1ad68f7feb62b60a73743589438dd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0.8"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.17-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php80\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ion Bazan",
|
||||
"email": "ion.bazan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-12T16:47:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v2.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "66a8f0957a3ca54e4f724e49028ab19d75a8918b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/66a8f0957a3ca54e4f724e49028ab19d75a8918b",
|
||||
"reference": "66a8f0957a3ca54e4f724e49028ab19d75a8918b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"psr/container": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/service-implementation": ""
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Contracts\\Service\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Generic abstractions related to writing services",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"abstractions",
|
||||
"contracts",
|
||||
"decoupling",
|
||||
"interfaces",
|
||||
"interoperability",
|
||||
"standards"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-20T17:43:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v5.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "90c2a5103f07feb19069379f3abdcdbacc7753a9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/90c2a5103f07feb19069379f3abdcdbacc7753a9",
|
||||
"reference": "90c2a5103f07feb19069379f3abdcdbacc7753a9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-intl-grapheme": "~1.0",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/polyfill-php80": "~1.15"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/error-handler": "^4.4|^5.0",
|
||||
"symfony/http-client": "^4.4|^5.0",
|
||||
"symfony/translation-contracts": "^1.1|^2",
|
||||
"symfony/var-exporter": "^4.4|^5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\String\\": ""
|
||||
},
|
||||
"files": [
|
||||
"Resources/functions.php"
|
||||
],
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony String component",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"grapheme",
|
||||
"i18n",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utf8"
|
||||
],
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-05-20T17:43:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "vlucas/phpdotenv",
|
||||
"version": "v4.1.5",
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
<?php //empty on purpose
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE `migrations` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`migration` varchar(255) NOT NULL,
|
||||
`type` enum('structure', 'data') NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -0,0 +1,17 @@
|
||||
ALTER TABLE
|
||||
`maps`
|
||||
MODIFY
|
||||
`bound_south_lat` decimal(9, 7) NOT NULL,
|
||||
MODIFY
|
||||
`bound_west_lng` decimal(10, 7) NOT NULL,
|
||||
MODIFY
|
||||
`bound_north_lat` decimal(9, 7) NOT NULL,
|
||||
MODIFY
|
||||
`bound_east_lng` decimal(10, 7) NOT NULL;
|
||||
|
||||
ALTER TABLE
|
||||
`places`
|
||||
MODIFY
|
||||
`lat` decimal(9, 7) NOT NULL,
|
||||
MODIFY
|
||||
`lng` decimal(10, 7) NOT NULL;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE
|
||||
`places`
|
||||
ADD
|
||||
`pano_id_cached` varchar(255) NULL DEFAULT NULL,
|
||||
ADD
|
||||
`pano_id_cached_timestamp` timestamp NULL DEFAULT NULL;
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE `users` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`email` varchar(100) NOT NULL,
|
||||
`password` varchar(60) NOT NULL,
|
||||
`type` enum('user', 'admin') NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `email` (`email`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
|
||||
@@ -3,6 +3,9 @@
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
const ROOT = __DIR__;
|
||||
const VERSION = '';
|
||||
const REVISION = '';
|
||||
const REVISION_DATE = '';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(ROOT);
|
||||
$dotenv->load();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require 'main.php';
|
||||
|
||||
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
|
||||
|
||||
$app->add(new MapGuesser\Cli\DatabaseMigration());
|
||||
$app->add(new MapGuesser\Cli\AddUserCommand());
|
||||
|
||||
$app->run();
|
||||
+34
-13
@@ -12,16 +12,20 @@ if (($pos = strpos($url, '?')) !== false) {
|
||||
$url = rawurldecode($url);
|
||||
|
||||
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
|
||||
Container::$routeCollection->get('login', 'login', [MapGuesser\Controller\LoginController::class, 'getLoginForm']);
|
||||
Container::$routeCollection->post('login-action', 'login', [MapGuesser\Controller\LoginController::class, 'login']);
|
||||
Container::$routeCollection->get('logout', 'logout', [MapGuesser\Controller\LoginController::class, 'logout']);
|
||||
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
|
||||
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
|
||||
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
|
||||
$routeCollection->get('position-json', '{mapId}/position.json', [MapGuesser\Controller\PositionController::class, 'getPosition']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\PositionController::class, 'evaluateGuess']);
|
||||
$routeCollection->get('newPlace-json', '{mapId}/newPlace.json', [MapGuesser\Controller\GameFlowController::class, 'getNewPlace']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'evaluateGuess']);
|
||||
});
|
||||
Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('admin.maps', 'maps', [MapGuesser\Controller\MapAdminController::class, 'getMaps']);
|
||||
$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->post('admin.saveMap', 'saveMap/{mapId}/json', [MapGuesser\Controller\MapAdminController::class, 'saveMap']);
|
||||
});
|
||||
|
||||
$match = Container::$routeCollection->match($method, explode('/', $url));
|
||||
@@ -29,15 +33,32 @@ $match = Container::$routeCollection->match($method, explode('/', $url));
|
||||
if ($match !== null) {
|
||||
list($route, $params) = $match;
|
||||
|
||||
$response = $route->callController($params);
|
||||
$request = new MapGuesser\Request\Request($_GET, $params, $_POST, $_SESSION);
|
||||
|
||||
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
|
||||
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
|
||||
echo $response->render();
|
||||
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
|
||||
header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode());
|
||||
$handler = $route->getHandler();
|
||||
$controller = new $handler[0]($request);
|
||||
|
||||
if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) {
|
||||
$authorized = $controller->authorize();
|
||||
} else {
|
||||
$authorized = true;
|
||||
}
|
||||
|
||||
if ($authorized) {
|
||||
$response = call_user_func([$controller, $handler[1]]);
|
||||
|
||||
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
|
||||
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
|
||||
echo $response->render();
|
||||
|
||||
return;
|
||||
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
|
||||
header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 404);
|
||||
require ROOT . '/views/error/404.php';
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=UTF-8', true, 404);
|
||||
require ROOT . '/views/error/404.php';
|
||||
@@ -1,13 +1,3 @@
|
||||
#roundInfo {
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#roundInfo p {
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
#panorama {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
@@ -110,6 +100,9 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
#mapName {
|
||||
display: none;
|
||||
}
|
||||
#showGuessButtonContainer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
#metadata {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 10px;
|
||||
padding: 10px;
|
||||
background-color: #eeeeee;
|
||||
border: solid 1px #555555;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
opacity: 0.95;
|
||||
z-index: 2;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
height: calc(100% - 40px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#map.selected {
|
||||
height: calc(50% - 25px);
|
||||
.leaflet-container {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
#panorama {
|
||||
width: 100%;
|
||||
height: calc(50% - 25px);
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#noPano {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(50% - 25px);
|
||||
z-index: 2;
|
||||
visibility: hidden;
|
||||
background: #cccccc;
|
||||
@@ -33,8 +42,8 @@
|
||||
|
||||
#control {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
top: 60px;
|
||||
width: 125px;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -42,8 +51,56 @@
|
||||
#placeControl {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: calc(50% + 35px);
|
||||
width: 100px;
|
||||
z-index: 3;
|
||||
width: 100px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#deleteButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 999px) and (min-height: 600px) {
|
||||
#metadata {
|
||||
width: calc(100% - 155px);
|
||||
}
|
||||
#map.selected {
|
||||
height: calc(50% - 20px);
|
||||
}
|
||||
#panorama, #noPano {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: calc(50% - 20px);
|
||||
}
|
||||
#placeControl {
|
||||
top: calc(50% + 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px), (max-height: 599px) {
|
||||
#metadata {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
#metadata.selected {
|
||||
top: 95px;
|
||||
}
|
||||
#map.selected {
|
||||
width: 50%;
|
||||
}
|
||||
#panorama, #noPano {
|
||||
top: 40px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
}
|
||||
#placeControl {
|
||||
top: 50px;
|
||||
}
|
||||
#modified.selected {
|
||||
right: calc(50% + 10px);
|
||||
}
|
||||
#control.selected {
|
||||
right: calc(50% + 10px);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ button::-moz-focus-inner, input::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
p, h1, h2, button, a {
|
||||
p, h1, h2, input, textarea, select, button, a {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
@@ -93,8 +93,12 @@ sub {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
svg.inline, img.inline {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
@@ -142,6 +146,16 @@ button.fullWidth, a.button.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button.noLeftRadius, a.button.noLeftRadius {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
button.noRightRadius, a.button.noRightRadius {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
button.gray, a.button.gray {
|
||||
background-color: #808080;
|
||||
}
|
||||
@@ -158,6 +172,70 @@ button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
|
||||
background-color: #7f2929;
|
||||
}
|
||||
|
||||
button.yellow, a.button.yellow {
|
||||
background-color: #e8a349;
|
||||
}
|
||||
|
||||
button.yellow:hover, button.yellow:focus, a.button.yellow:hover, a.button.yellow:focus {
|
||||
background-color: #c37713;
|
||||
}
|
||||
|
||||
button.green, a.button.green {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
button.green:hover, button.green:focus, a.button.green:hover, a.button.green:focus {
|
||||
background-color: #1b7d31;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
background-color: #f9fafb;
|
||||
border: solid #c8d2e1 1px;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: 13px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input.big, select.big, textarea.big {
|
||||
padding: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input.fullWidth, select.fullWidth, textarea.fullWidth {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:disabled, select:disabled, textarea:disabled {
|
||||
background-color: #dfdfdf;
|
||||
border: solid #dfdfdf 1px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
background-color: #ffffff;
|
||||
border: solid #29457f 2px;
|
||||
padding: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input.big:focus, select.big:focus, textarea.big:focus {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
p.formError {
|
||||
color: #7f2929;
|
||||
font-weight: 500;
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.header {
|
||||
background-color: #333333;
|
||||
height: 50px;
|
||||
@@ -176,6 +254,24 @@ div.header.small {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
div.header>div.grid>:nth-child(2) {
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.header>div.grid>:nth-child(2)>span {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
div.header>div.grid>:nth-child(2)>span>a:link, div.header>div.grid>:nth-child(2)>span>a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
div.header>div.grid>:nth-child(2)>span:not(:last-child) {
|
||||
border-right: solid white 1px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
div.main {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
@@ -189,7 +285,7 @@ div.buttonContainer>button {
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
top: 50%;
|
||||
@@ -200,12 +296,24 @@ div.buttonContainer>button {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
div.box {
|
||||
width: 576px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
div.header.small h1 span {
|
||||
display: none;
|
||||
}
|
||||
button {
|
||||
button, a.button {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
div.box {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,15 @@
|
||||
|
||||
div.mapItem {
|
||||
width: 350px;
|
||||
background-color: #eeeeee;
|
||||
border-radius: 3px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
div.mapItem.new {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
@@ -27,7 +31,8 @@ div.mapItem>img {
|
||||
}
|
||||
|
||||
div.mapItem>div.inner {
|
||||
padding: 8px;
|
||||
background-color: #eeeeee;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
div.mapItem>div.inner>div.info {
|
||||
@@ -43,6 +48,12 @@ div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.mapItem>div.buttonContainer {
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
#mapContainer {
|
||||
grid-template-columns: auto auto auto auto;
|
||||
@@ -69,6 +80,6 @@ div.mapItem>div.inner>div.info>p:nth-child(2) {
|
||||
|
||||
@media screen and (max-width: 374px) {
|
||||
div.mapItem {
|
||||
width: initial;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+96
-97
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
var Core = {
|
||||
var Game = {
|
||||
NUMBER_OF_ROUNDS: 5,
|
||||
MAX_SCORE: 1000,
|
||||
|
||||
@@ -17,54 +17,54 @@
|
||||
initialize: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
document.getElementById('cover').style.visibility = 'visible';
|
||||
document.getElementById('currentRound').innerHTML = '1/' + String(Core.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = '0/0';
|
||||
|
||||
Core.map.setOptions({
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'crosshair'
|
||||
});
|
||||
Core.map.fitBounds(mapBounds);
|
||||
Game.map.fitBounds(mapBounds);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
//TODO: handle this error
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
document.getElementById('cover').style.visibility = 'hidden';
|
||||
|
||||
Core.panoId = this.response.panoId;
|
||||
Game.panoId = this.response.panoId;
|
||||
|
||||
if (this.response.history) {
|
||||
for (var i = 0; i < this.response.history.length; ++i) {
|
||||
var round = this.response.history[i];
|
||||
Core.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
|
||||
Core.addRealGuessPair(round.position, round.guessPosition, true);
|
||||
Core.scoreSum += round.score;
|
||||
Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
|
||||
Game.addRealGuessPair(round.position, round.guessPosition, true);
|
||||
Game.scoreSum += round.score;
|
||||
}
|
||||
|
||||
document.getElementById('currentRound').innerHTML = String(Core.rounds.length) + '/' + String(Core.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_SCORE);
|
||||
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);
|
||||
}
|
||||
|
||||
Core.startNewRound();
|
||||
Game.startNewRound();
|
||||
};
|
||||
|
||||
xhr.open('GET', '/game/' + mapId + '/position.json', true);
|
||||
xhr.open('GET', '/game/' + mapId + '/newPlace.json', true);
|
||||
xhr.send();
|
||||
},
|
||||
|
||||
resetGame: function () {
|
||||
if (Core.guessMarker) {
|
||||
Core.guessMarker.setMap(null);
|
||||
Core.guessMarker = null;
|
||||
reset: function () {
|
||||
if (Game.guessMarker) {
|
||||
Game.guessMarker.setMap(null);
|
||||
Game.guessMarker = null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Core.rounds.length; ++i) {
|
||||
var round = Core.rounds[i];
|
||||
for (var i = 0; i < Game.rounds.length; ++i) {
|
||||
var round = Game.rounds[i];
|
||||
|
||||
if (round.realMarker && round.guessMarker && round.line) {
|
||||
round.realMarker.setMap(null);
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
Core.rounds = [];
|
||||
Core.scoreSum = 0;
|
||||
Game.rounds = [];
|
||||
Game.scoreSum = 0;
|
||||
|
||||
var distanceInfo = document.getElementById('distanceInfo');
|
||||
distanceInfo.children[0].style.display = null;
|
||||
@@ -89,14 +89,14 @@
|
||||
document.getElementById('guess').style.visibility = null;
|
||||
document.getElementById('guess').classList.remove('result');
|
||||
|
||||
Core.initialize();
|
||||
Game.initialize();
|
||||
},
|
||||
|
||||
resetRound: function () {
|
||||
document.getElementById('scoreBar').style.width = null;
|
||||
|
||||
if (Core.rounds.length > 0) {
|
||||
var lastRound = Core.rounds[Core.rounds.length - 1];
|
||||
if (Game.rounds.length > 0) {
|
||||
var lastRound = Game.rounds[Game.rounds.length - 1];
|
||||
|
||||
lastRound.realMarker.setVisible(false);
|
||||
lastRound.guessMarker.setVisible(false);
|
||||
@@ -108,20 +108,20 @@
|
||||
document.getElementById('guess').style.visibility = null;
|
||||
document.getElementById('guess').classList.remove('result')
|
||||
|
||||
Core.map.setOptions({
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'crosshair'
|
||||
});
|
||||
Core.map.fitBounds(mapBounds);
|
||||
Game.map.fitBounds(mapBounds);
|
||||
|
||||
Core.startNewRound();
|
||||
Game.startNewRound();
|
||||
},
|
||||
|
||||
startNewRound: function () {
|
||||
Core.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarker: null, line: null });
|
||||
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarker: null, line: null });
|
||||
|
||||
document.getElementById('currentRound').innerHTML = String(Core.rounds.length) + '/' + String(Core.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
|
||||
Core.loadPano(Core.panoId);
|
||||
Game.loadPano(Game.panoId);
|
||||
},
|
||||
|
||||
handleErrorResponse: function (error) {
|
||||
@@ -132,28 +132,33 @@
|
||||
xhr.onload = function () {
|
||||
mapBounds = this.response.bounds;
|
||||
|
||||
Core.resetGame();
|
||||
Game.reset();
|
||||
};
|
||||
|
||||
xhr.open('GET', '/game/' + mapId + '/position.json', true);
|
||||
xhr.open('GET', '/game/' + mapId + '/json', true);
|
||||
xhr.send();
|
||||
},
|
||||
|
||||
loadPano: function (panoId) {
|
||||
if (Core.adaptGuess) {
|
||||
if (Game.adaptGuess) {
|
||||
document.getElementById('guess').classList.add('adapt');
|
||||
}
|
||||
|
||||
Core.panorama.setPov({ heading: 0, pitch: 0 });
|
||||
Core.panorama.setZoom(0);
|
||||
Core.panorama.setPano(panoId);
|
||||
Game.panorama.setPov({ heading: 0, pitch: 0 });
|
||||
Game.panorama.setZoom(0);
|
||||
Game.panorama.setPano(panoId);
|
||||
},
|
||||
|
||||
evaluateGuess: function () {
|
||||
var guessPosition = Core.guessMarker.getPosition().toJSON();
|
||||
Core.rounds[Core.rounds.length - 1].guessPosition = guessPosition;
|
||||
if (!Game.guessMarker) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Core.adaptGuess) {
|
||||
var guessPosition = Game.guessMarker.getPosition().toJSON();
|
||||
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
|
||||
|
||||
document.getElementById('guessButton').disabled = true;
|
||||
if (Game.adaptGuess) {
|
||||
document.getElementById('guess').classList.remove('adapt');
|
||||
}
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
@@ -167,45 +172,45 @@
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
if (this.response.error) {
|
||||
Core.handleErrorResponse(this.response.error);
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
Core.guessMarker.setMap(null);
|
||||
Core.guessMarker = null;
|
||||
Game.guessMarker.setMap(null);
|
||||
Game.guessMarker = null;
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
document.getElementById('guess').classList.add('result');
|
||||
|
||||
Core.scoreSum += this.response.result.score;
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_SCORE);
|
||||
Game.scoreSum += this.response.result.score;
|
||||
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
|
||||
|
||||
Core.rounds[Core.rounds.length - 1].position = this.response.result.position;
|
||||
Core.addRealGuessPair(this.response.result.position, guessPosition);
|
||||
Game.rounds[Game.rounds.length - 1].position = this.response.result.position;
|
||||
Game.addRealGuessPair(this.response.result.position, guessPosition);
|
||||
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
resultBounds.extend(this.response.result.position);
|
||||
resultBounds.extend(guessPosition);
|
||||
|
||||
Core.map.setOptions({
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'grab'
|
||||
});
|
||||
Core.map.fitBounds(resultBounds);
|
||||
Game.map.fitBounds(resultBounds);
|
||||
|
||||
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
|
||||
document.getElementById('score').innerHTML = this.response.result.score;
|
||||
|
||||
var scoreBarProperties = Core.calculateScoreBarProperties(this.response.result.score, Core.MAX_SCORE);
|
||||
var scoreBarProperties = Game.calculateScoreBarProperties(this.response.result.score, Game.MAX_SCORE);
|
||||
var scoreBar = document.getElementById('scoreBar');
|
||||
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
|
||||
scoreBar.style.width = scoreBarProperties.width;
|
||||
|
||||
if (Core.rounds.length === Core.NUMBER_OF_ROUNDS) {
|
||||
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
|
||||
document.getElementById('continueButton').style.display = 'none';
|
||||
document.getElementById('showSummaryButton').style.display = 'block';
|
||||
}
|
||||
|
||||
Core.panoId = this.response.panoId;
|
||||
Game.panoId = this.response.panoId;
|
||||
};
|
||||
|
||||
xhr.open('POST', '/game/' + mapId + '/guess.json', true);
|
||||
@@ -213,18 +218,18 @@
|
||||
},
|
||||
|
||||
addRealGuessPair: function (position, guessPosition, hidden) {
|
||||
var round = Core.rounds[Core.rounds.length - 1];
|
||||
var round = Game.rounds[Game.rounds.length - 1];
|
||||
|
||||
round.realMarker = new google.maps.Marker({
|
||||
map: Core.map,
|
||||
map: Game.map,
|
||||
visible: !hidden,
|
||||
position: position,
|
||||
title: 'Open in Google Maps',
|
||||
zIndex: Core.rounds.length * 2,
|
||||
zIndex: Game.rounds.length * 2,
|
||||
clickable: true,
|
||||
draggable: false,
|
||||
icon: {
|
||||
url: '/static/img/markers/marker-green.svg',
|
||||
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev=' + REVISION,
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32)
|
||||
@@ -236,14 +241,14 @@
|
||||
});
|
||||
|
||||
round.guessMarker = new google.maps.Marker({
|
||||
map: Core.map,
|
||||
map: Game.map,
|
||||
visible: !hidden,
|
||||
position: guessPosition,
|
||||
zIndex: Core.rounds.length,
|
||||
zIndex: Game.rounds.length,
|
||||
clickable: false,
|
||||
draggable: false,
|
||||
icon: {
|
||||
url: '/static/img/markers/marker-gray-empty.svg',
|
||||
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
@@ -259,7 +264,7 @@
|
||||
});
|
||||
|
||||
round.line = new google.maps.Polyline({
|
||||
map: Core.map,
|
||||
map: Game.map,
|
||||
visible: !hidden,
|
||||
path: [
|
||||
position,
|
||||
@@ -310,11 +315,11 @@
|
||||
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
|
||||
for (var i = 0; i < Core.rounds.length; ++i) {
|
||||
var round = Core.rounds[i];
|
||||
for (var i = 0; i < Game.rounds.length; ++i) {
|
||||
var round = Game.rounds[i];
|
||||
|
||||
round.realMarker.setIcon({
|
||||
url: '/static/img/markers/marker-green-empty.svg',
|
||||
url: STATIC_ROOT + '/img/markers/marker-green-empty.svg?rev=' + REVISION,
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
@@ -335,32 +340,32 @@
|
||||
resultBounds.extend(round.guessPosition);
|
||||
}
|
||||
|
||||
Core.map.fitBounds(resultBounds);
|
||||
Game.map.fitBounds(resultBounds);
|
||||
|
||||
document.getElementById('scoreSum').innerHTML = String(Core.scoreSum);
|
||||
document.getElementById('scoreSum').innerHTML = String(Game.scoreSum);
|
||||
|
||||
var scoreBarProperties = Core.calculateScoreBarProperties(Core.scoreSum, Core.NUMBER_OF_ROUNDS * Core.MAX_SCORE);
|
||||
var scoreBarProperties = Game.calculateScoreBarProperties(Game.scoreSum, Game.NUMBER_OF_ROUNDS * Game.MAX_SCORE);
|
||||
var scoreBar = document.getElementById('scoreBar');
|
||||
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
|
||||
scoreBar.style.width = scoreBarProperties.width;
|
||||
},
|
||||
|
||||
rewriteGoogleLink: function () {
|
||||
if (!Core.googleLink) {
|
||||
if (!Game.googleLink) {
|
||||
var anchors = document.getElementById('panorama').getElementsByTagName('a');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
var a = anchors[i];
|
||||
if (a.href.indexOf('maps.google.com/maps') !== -1) {
|
||||
Core.googleLink = a;
|
||||
Game.googleLink = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
if (Core.googleLink) {
|
||||
Core.googleLink.title = 'Google Maps';
|
||||
Core.googleLink.href = 'https://maps.google.com/maps';
|
||||
if (Game.googleLink) {
|
||||
Game.googleLink.title = 'Google Maps';
|
||||
Game.googleLink.href = 'https://maps.google.com/maps';
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
@@ -381,32 +386,32 @@
|
||||
};
|
||||
|
||||
if (!('ontouchstart' in document.documentElement)) {
|
||||
Core.adaptGuess = true;
|
||||
Game.adaptGuess = true;
|
||||
}
|
||||
|
||||
Core.map = new google.maps.Map(document.getElementById('map'), {
|
||||
Game.map = new google.maps.Map(document.getElementById('map'), {
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: false,
|
||||
draggingCursor: 'grabbing'
|
||||
});
|
||||
|
||||
Core.map.addListener('click', function (e) {
|
||||
if (Core.rounds[Core.rounds.length - 1].guessPosition) {
|
||||
Game.map.addListener('click', function (e) {
|
||||
if (Game.rounds[Game.rounds.length - 1].guessPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Core.guessMarker) {
|
||||
Core.guessMarker.setPosition(e.latLng);
|
||||
if (Game.guessMarker) {
|
||||
Game.guessMarker.setPosition(e.latLng);
|
||||
return;
|
||||
}
|
||||
|
||||
Core.guessMarker = new google.maps.Marker({
|
||||
map: Core.map,
|
||||
Game.guessMarker = new google.maps.Marker({
|
||||
map: Game.map,
|
||||
position: e.latLng,
|
||||
clickable: false,
|
||||
draggable: true,
|
||||
icon: {
|
||||
url: '/static/img/markers/marker-gray-empty.svg',
|
||||
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
|
||||
size: new google.maps.Size(24, 32),
|
||||
scaledSize: new google.maps.Size(24, 32),
|
||||
anchor: new google.maps.Point(12, 32),
|
||||
@@ -424,22 +429,22 @@
|
||||
document.getElementById('guessButton').disabled = false;
|
||||
});
|
||||
|
||||
Core.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
||||
Game.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
||||
disableDefaultUI: true,
|
||||
linksControl: true,
|
||||
showRoadLabels: false,
|
||||
motionTracking: false
|
||||
});
|
||||
|
||||
Core.panorama.addListener('position_changed', function () {
|
||||
Core.rewriteGoogleLink();
|
||||
Game.panorama.addListener('position_changed', function () {
|
||||
Game.rewriteGoogleLink();
|
||||
});
|
||||
|
||||
Core.panorama.addListener('pov_changed', function () {
|
||||
Core.rewriteGoogleLink();
|
||||
Game.panorama.addListener('pov_changed', function () {
|
||||
Game.rewriteGoogleLink();
|
||||
});
|
||||
|
||||
Core.initialize();
|
||||
Game.initialize();
|
||||
|
||||
document.getElementById('showGuessButton').onclick = function () {
|
||||
this.style.visibility = 'hidden';
|
||||
@@ -452,24 +457,18 @@
|
||||
}
|
||||
|
||||
document.getElementById('guessButton').onclick = function () {
|
||||
if (!Core.guessMarker) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disabled = true;
|
||||
|
||||
Core.evaluateGuess();
|
||||
Game.evaluateGuess();
|
||||
}
|
||||
|
||||
document.getElementById('continueButton').onclick = function () {
|
||||
Core.resetRound();
|
||||
Game.resetRound();
|
||||
}
|
||||
|
||||
document.getElementById('showSummaryButton').onclick = function () {
|
||||
Core.showSummary();
|
||||
Game.showSummary();
|
||||
}
|
||||
|
||||
document.getElementById('startNewGameButton').onclick = function () {
|
||||
Core.resetGame();
|
||||
Game.reset();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,42 @@
|
||||
(function () {
|
||||
var form = document.getElementById('loginForm');
|
||||
|
||||
form.onsubmit = function (e) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var formData = new FormData(form);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
if (this.response.error) {
|
||||
var errorText;
|
||||
switch (this.response.error) {
|
||||
case 'user_not_found':
|
||||
errorText = 'No user found with the given email address.';
|
||||
break;
|
||||
case 'password_not_match':
|
||||
errorText = 'The given password is wrong.'
|
||||
break;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
var loginFormError = document.getElementById('loginFormError');
|
||||
loginFormError.style.display = 'block';
|
||||
loginFormError.innerHTML = errorText;
|
||||
|
||||
form.elements.email.select();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.replace('/');
|
||||
};
|
||||
|
||||
xhr.open('POST', form.action, true);
|
||||
xhr.send(formData);
|
||||
};
|
||||
})();
|
||||
+400
-48
@@ -1,91 +1,416 @@
|
||||
'use strict';
|
||||
|
||||
(function () {
|
||||
var MapEditor = {
|
||||
metadata: {
|
||||
name: null,
|
||||
description: null
|
||||
},
|
||||
map: null,
|
||||
markers: null,
|
||||
panorama: null,
|
||||
selectedMarker: null,
|
||||
added: {},
|
||||
edited: {},
|
||||
deleted: {},
|
||||
|
||||
loadPano: function (data, status) {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
editMetadata: function () {
|
||||
var form = document.getElementById('metadataForm');
|
||||
|
||||
MapEditor.metadata.name = form.elements.name.value;
|
||||
MapEditor.metadata.description = form.elements.description.value;
|
||||
|
||||
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
|
||||
|
||||
document.getElementById('metadata').style.visibility = 'hidden';
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
getPlace: function (placeId, marker) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (!this.response.panoId) {
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
|
||||
places[marker.placeId].panoId = -1;
|
||||
places[marker.placeId].noPano = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
places[marker.placeId].panoId = this.response.panoId;
|
||||
places[marker.placeId].noPano = false;
|
||||
|
||||
MapEditor.loadPano(this.response.panoId, places[marker.placeId].pov);
|
||||
};
|
||||
|
||||
xhr.open('GET', '/admin/place.json/' + placeId, true);
|
||||
xhr.send();
|
||||
},
|
||||
|
||||
loadPano: function (panoId, pov) {
|
||||
MapEditor.panorama.setVisible(true);
|
||||
MapEditor.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
|
||||
MapEditor.panorama.setZoom(pov.zoom);
|
||||
MapEditor.panorama.setPano(panoId);
|
||||
},
|
||||
|
||||
loadPanoForNewPlace: function (panoLocationData) {
|
||||
var placeId = MapEditor.selectedMarker.placeId;
|
||||
|
||||
if (!panoLocationData) {
|
||||
places[placeId].panoId = -1;
|
||||
places[placeId].noPano = true;
|
||||
|
||||
if (status !== google.maps.StreetViewStatus.OK) {
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var latLng = panoLocationData.latLng;
|
||||
|
||||
places[placeId].panoId = panoLocationData.pano;
|
||||
places[placeId].lat = latLng.lat();
|
||||
places[placeId].lng = latLng.lng();
|
||||
|
||||
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
|
||||
MapEditor.map.panTo(MapEditor.selectedMarker.getLatLng());
|
||||
|
||||
MapEditor.panorama.setVisible(true);
|
||||
MapEditor.panorama.setPov({ heading: 0, pitch: 0 });
|
||||
MapEditor.panorama.setZoom(0);
|
||||
MapEditor.panorama.setPano(data.location.pano);
|
||||
MapEditor.panorama.setPov({ heading: 0.0, pitch: 0.0 });
|
||||
MapEditor.panorama.setZoom(0.0);
|
||||
MapEditor.panorama.setPano(panoLocationData.pano);
|
||||
},
|
||||
|
||||
requestPanoData: function (location, canBeIndoor) {
|
||||
var sv = new google.maps.StreetViewService();
|
||||
|
||||
sv.getPanorama({
|
||||
location: location,
|
||||
preference: google.maps.StreetViewPreference.NEAREST,
|
||||
radius: 100,
|
||||
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
|
||||
}, function (data, status) {
|
||||
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
|
||||
|
||||
if (panoLocationData === null && !canBeIndoor) {
|
||||
MapEditor.requestPanoData(location, true);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
MapEditor.loadPanoForNewPlace(panoLocationData);
|
||||
});
|
||||
},
|
||||
|
||||
select: function (marker) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
if (MapEditor.selectedMarker === marker) {
|
||||
MapEditor.closePlace();
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('metadata').classList.add('selected');
|
||||
document.getElementById('map').classList.add('selected');
|
||||
document.getElementById('control').classList.add('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.display = 'block';
|
||||
document.getElementById('panorama').style.visibility = 'visible';
|
||||
document.getElementById('placeControl').style.visibility = 'visible';
|
||||
|
||||
MapEditor.resetSelected();
|
||||
MapEditor.selectedMarker = marker;
|
||||
|
||||
marker.setIcon(L.icon({
|
||||
iconUrl: '/static/img/markers/marker-blue.svg',
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}));
|
||||
marker.setZIndexOffset(2000);
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
MapEditor.map.panTo(marker.getLatLng());
|
||||
|
||||
MapEditor.panorama.setVisible(false);
|
||||
|
||||
var sv = new google.maps.StreetViewService();
|
||||
sv.getPanorama({ location: marker.getLatLng(), preference: google.maps.StreetViewPreference.NEAREST, source: google.maps.StreetViewSource.OUTDOOR }, MapEditor.loadPano);
|
||||
if (marker.placeId) {
|
||||
MapEditor.markers.removeLayer(MapEditor.selectedMarker);
|
||||
MapEditor.map.addLayer(MapEditor.selectedMarker);
|
||||
marker.setIcon(IconCollection.iconBlue);
|
||||
marker.setZIndexOffset(2000);
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'block';
|
||||
|
||||
if (places[marker.placeId].panoId) {
|
||||
if (places[marker.placeId].panoId === -1) {
|
||||
document.getElementById('noPano').style.visibility = 'visible';
|
||||
} else {
|
||||
MapEditor.loadPano(places[marker.placeId].panoId, places[marker.placeId].pov);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapEditor.getPlace(marker.placeId, marker);
|
||||
} else {
|
||||
marker.placeId = 'new_' + new Date().getTime();
|
||||
|
||||
var latLng = marker.getLatLng();
|
||||
|
||||
places[marker.placeId] = { id: null, lat: latLng.lat, lng: latLng.lng, panoId: null, pov: { heading: 0.0, pitch: 0.0, zoom: 0 }, noPano: false };
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapEditor.requestPanoData(latLng);
|
||||
}
|
||||
},
|
||||
|
||||
resetSelected: function () {
|
||||
resetSelected: function (del) {
|
||||
if (!MapEditor.selectedMarker) {
|
||||
return;
|
||||
}
|
||||
|
||||
MapEditor.selectedMarker.setIcon(L.icon({
|
||||
iconUrl: '/static/img/markers/marker-green.svg',
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}));
|
||||
MapEditor.selectedMarker.setZIndexOffset(1000);
|
||||
var placeId = MapEditor.selectedMarker.placeId
|
||||
|
||||
if (places[placeId].id && !del) {
|
||||
MapEditor.map.removeLayer(MapEditor.selectedMarker);
|
||||
MapEditor.markers.addLayer(MapEditor.selectedMarker);
|
||||
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
|
||||
MapEditor.selectedMarker.setZIndexOffset(1000);
|
||||
} else {
|
||||
delete places[placeId];
|
||||
MapEditor.map.removeLayer(MapEditor.selectedMarker);
|
||||
}
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'none';
|
||||
},
|
||||
|
||||
applyPlace: function () {
|
||||
var placeId = MapEditor.selectedMarker.placeId;
|
||||
|
||||
if (!places[placeId].noPano) {
|
||||
var latLng = MapEditor.panorama.getPosition();
|
||||
var pov = MapEditor.panorama.getPov();
|
||||
var zoom = MapEditor.panorama.getZoom();
|
||||
|
||||
places[placeId].lat = latLng.lat();
|
||||
places[placeId].lng = latLng.lng();
|
||||
places[placeId].panoId = MapEditor.panorama.getPano();
|
||||
places[placeId].pov = { heading: pov.heading, pitch: pov.pitch, zoom: zoom };
|
||||
}
|
||||
|
||||
if (!places[placeId].id) {
|
||||
places[placeId].id = placeId;
|
||||
MapEditor.added[placeId] = places[placeId];
|
||||
|
||||
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
|
||||
|
||||
document.getElementById('deleteButton').style.display = 'block';
|
||||
} else {
|
||||
if (!MapEditor.added[placeId]) {
|
||||
MapEditor.edited[placeId] = places[placeId];
|
||||
|
||||
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
|
||||
} else {
|
||||
MapEditor.added[placeId] = places[placeId];
|
||||
}
|
||||
}
|
||||
|
||||
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
closePlace: function (del) {
|
||||
document.getElementById('metadata').classList.remove('selected')
|
||||
document.getElementById('map').classList.remove('selected');
|
||||
document.getElementById('control').classList.remove('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.visibility = 'hidden';
|
||||
document.getElementById('placeControl').style.visibility = 'hidden';
|
||||
|
||||
MapEditor.resetSelected(del);
|
||||
MapEditor.selectedMarker = null;
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
},
|
||||
|
||||
deletePlace: function () {
|
||||
var placeId = MapEditor.selectedMarker.placeId;
|
||||
|
||||
if (places[placeId].id && !MapEditor.added[placeId]) {
|
||||
MapEditor.deleted[placeId] = places[placeId];
|
||||
|
||||
document.getElementById('deleted').innerHTML = String(Object.keys(MapEditor.deleted).length);
|
||||
}
|
||||
|
||||
MapEditor.closePlace(true);
|
||||
|
||||
delete MapEditor.added[placeId];
|
||||
delete MapEditor.edited[placeId];
|
||||
|
||||
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
|
||||
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
|
||||
|
||||
document.getElementById('saveButton').disabled = false;
|
||||
},
|
||||
|
||||
saveMap: function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
var data = new FormData();
|
||||
|
||||
if (MapEditor.metadata.name !== null) {
|
||||
data.append('name', MapEditor.metadata.name);
|
||||
}
|
||||
if (MapEditor.metadata.description !== null) {
|
||||
data.append('description', MapEditor.metadata.description);
|
||||
}
|
||||
|
||||
for (var placeId in MapEditor.added) {
|
||||
if (!MapEditor.added.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
data.append('added[]', JSON.stringify(MapEditor.added[placeId]));
|
||||
}
|
||||
for (var placeId in MapEditor.edited) {
|
||||
if (!MapEditor.edited.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
data.append('edited[]', JSON.stringify(MapEditor.edited[placeId]));
|
||||
}
|
||||
for (var placeId in MapEditor.deleted) {
|
||||
if (!MapEditor.deleted.hasOwnProperty(placeId)) {
|
||||
continue;
|
||||
}
|
||||
data.append('deleted[]', JSON.stringify(MapEditor.deleted[placeId]));
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
if (this.response.error) {
|
||||
//TODO: handle this error
|
||||
return;
|
||||
}
|
||||
|
||||
MapEditor.replacePlaceIdsToReal(this.response.added);
|
||||
|
||||
if (mapId === 0) {
|
||||
mapId = this.response.mapId;
|
||||
window.history.replaceState(null, '', '/admin/mapEditor/' + mapId);
|
||||
}
|
||||
|
||||
MapEditor.added = {};
|
||||
MapEditor.edited = {};
|
||||
MapEditor.deleted = {};
|
||||
|
||||
document.getElementById('added').innerHTML = '0';
|
||||
document.getElementById('edited').innerHTML = '0';
|
||||
document.getElementById('deleted').innerHTML = '0';
|
||||
|
||||
document.getElementById('saveButton').disabled = true;
|
||||
};
|
||||
|
||||
xhr.open('POST', '/admin/saveMap/' + mapId + '/json', true);
|
||||
xhr.send(data);
|
||||
},
|
||||
|
||||
replacePlaceIdsToReal: function (addedPlaces) {
|
||||
for (var i = 0; i < addedPlaces.length; ++i) {
|
||||
var tempId = addedPlaces[i].tempId;
|
||||
var placeId = addedPlaces[i].id;
|
||||
places[tempId].id = placeId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var IconCollection = {
|
||||
iconGreen: L.icon({
|
||||
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
iconRed: L.icon({
|
||||
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
iconBlue: L.icon({
|
||||
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
};
|
||||
|
||||
var Util = {
|
||||
getHighResData: function () {
|
||||
if (window.devicePixelRatio >= 4) {
|
||||
return { ppi: 320, tileSize: 128, zoomOffset: 1 };
|
||||
} else if (window.devicePixelRatio >= 2) {
|
||||
return { ppi: 250, tileSize: 256, zoomOffset: 0 };
|
||||
} else {
|
||||
return { ppi: 72, tileSize: 512, zoomOffset: -1 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MapEditor.map = L.map('map', {
|
||||
attributionControl: false,
|
||||
zoomControl: false
|
||||
});
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
minZoom: 0,
|
||||
maxZoom: 20
|
||||
}).addTo(MapEditor.map);
|
||||
|
||||
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||
|
||||
for (var i = 0; i < places.length; ++i) {
|
||||
var marker = L.marker({ lat: places[i].lat, lng: places[i].lng }, {
|
||||
icon: L.icon({
|
||||
iconUrl: '/static/img/markers/marker-green.svg',
|
||||
iconSize: [24, 32],
|
||||
iconAnchor: [12, 32]
|
||||
}),
|
||||
zIndexOffset: 1000
|
||||
MapEditor.map.on('click', function (e) {
|
||||
var marker = L.marker(e.latlng, {
|
||||
icon: IconCollection.iconBlue,
|
||||
zIndexOffset: 2000
|
||||
})
|
||||
.addTo(MapEditor.map)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
MapEditor.select(marker);
|
||||
});
|
||||
|
||||
var highResData = Util.getHighResData();
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
attribution: tileAttribution,
|
||||
subdomains: '1234',
|
||||
ppi: highResData.ppi,
|
||||
tileSize: highResData.tileSize,
|
||||
zoomOffset: highResData.zoomOffset,
|
||||
minZoom: 2,
|
||||
maxZoom: 20
|
||||
}).addTo(MapEditor.map);
|
||||
|
||||
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
|
||||
|
||||
MapEditor.markers = L.markerClusterGroup({
|
||||
maxClusterRadius: 50
|
||||
});
|
||||
|
||||
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 ? IconCollection.iconRed : IconCollection.iconGreen,
|
||||
zIndexOffset: 1000
|
||||
})
|
||||
.addTo(MapEditor.markers)
|
||||
.on('click', function () {
|
||||
MapEditor.select(this);
|
||||
});
|
||||
|
||||
marker.placeId = place.id;
|
||||
}
|
||||
|
||||
MapEditor.map.addLayer(MapEditor.markers);
|
||||
|
||||
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
|
||||
// switch off fullscreenControl because positioning doesn't work
|
||||
fullscreenControl: false,
|
||||
@@ -95,15 +420,42 @@
|
||||
motionTracking: false
|
||||
});
|
||||
|
||||
document.getElementById('cancelButton').onclick = function () {
|
||||
document.getElementById('map').classList.remove('selected');
|
||||
document.getElementById('noPano').style.visibility = 'hidden';
|
||||
document.getElementById('panorama').style.display = 'none';
|
||||
document.getElementById('placeControl').style.visibility = 'hidden';
|
||||
document.getElementById('mapName').onclick = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
MapEditor.resetSelected();
|
||||
MapEditor.selectedMarker = null;
|
||||
var metadata = document.getElementById('metadata');
|
||||
|
||||
MapEditor.map.invalidateSize(true);
|
||||
if (metadata.style.visibility === 'visible') {
|
||||
metadata.style.visibility = 'hidden';
|
||||
} else {
|
||||
metadata.style.visibility = 'visible';
|
||||
document.getElementById('metadataForm').elements.name.select();
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('metadataForm').onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
MapEditor.editMetadata();
|
||||
};
|
||||
|
||||
document.getElementById('closeMetadataButton').onclick = function () {
|
||||
document.getElementById('metadata').style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
document.getElementById('saveButton').onclick = function () {
|
||||
MapEditor.saveMap();
|
||||
};
|
||||
|
||||
document.getElementById('applyButton').onclick = function () {
|
||||
MapEditor.applyPlace();
|
||||
};
|
||||
|
||||
document.getElementById('closeButton').onclick = function () {
|
||||
MapEditor.closePlace();
|
||||
};
|
||||
|
||||
document.getElementById('deleteButton').onclick = function () {
|
||||
MapEditor.deletePlace();
|
||||
};
|
||||
})();
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"leaflet": "^1.6.0"
|
||||
"leaflet": "^1.6.0",
|
||||
"leaflet.markercluster": "^1.4.1"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
leaflet.markercluster@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5"
|
||||
integrity sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==
|
||||
|
||||
leaflet@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
|
||||
|
||||
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
WORKTREE_REGEX = r"^worktree (.*)\nHEAD ([a-f0-9]*)\n(?:branch refs\/heads\/(.*)|detached)$"
|
||||
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: ./deploy-to-multiple-worktrees.py REPO_PATH WORKTREE_DEVELOPMENT_PATH WORKTREE_PRODUCTION_PATH")
|
||||
exit(1)
|
||||
|
||||
REPO = os.path.abspath(sys.argv[1])
|
||||
WORKTREE_DEVELOPMENT = os.path.abspath(sys.argv[2])
|
||||
WORKTREE_PRODUCTION = os.path.abspath(sys.argv[3])
|
||||
|
||||
class Worktree:
|
||||
def __init__(self, path, branch, revision, version):
|
||||
self.path = path
|
||||
self.branch = branch
|
||||
self.revision = revision
|
||||
self.version = version
|
||||
self.newRevision = None
|
||||
self.newVersion = None
|
||||
|
||||
def getDataForWorktrees():
|
||||
ret = subprocess.check_output(["git", "worktree", "list", "--porcelain"], cwd=REPO).decode().strip()
|
||||
blocks = ret.split("\n\n")
|
||||
|
||||
worktrees = []
|
||||
|
||||
for block in blocks:
|
||||
matches = re.search(WORKTREE_REGEX, block)
|
||||
|
||||
if matches:
|
||||
path = matches.group(1)
|
||||
revision = matches.group(2)
|
||||
branch = matches.group(3)
|
||||
version = getVersion(revision)
|
||||
|
||||
worktrees.append(Worktree(path, branch, revision, version))
|
||||
|
||||
return worktrees
|
||||
|
||||
def findWorktree(path):
|
||||
for worktree in worktrees:
|
||||
if worktree.path == path:
|
||||
return worktree
|
||||
|
||||
return None
|
||||
|
||||
def getVersion(branch):
|
||||
return subprocess.check_output(["git", "describe", "--tags", "--always", "--match", "Release_*", branch], cwd=REPO).decode().strip()
|
||||
|
||||
def getRevisionForRef(ref):
|
||||
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
|
||||
|
||||
def getLatestReleaseTag():
|
||||
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
|
||||
|
||||
def updateRepoFromRemote():
|
||||
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
|
||||
|
||||
def checkoutWorktree(worktreePath, ref):
|
||||
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
|
||||
|
||||
def cleanWorktree(worktreePath):
|
||||
subprocess.call(["git", "clean", "-f", "-d"], cwd=worktreePath)
|
||||
|
||||
def updateAppInWorktree(worktreePath):
|
||||
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
|
||||
|
||||
def updateAppVersionInWorktree(worktreePath):
|
||||
subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
|
||||
|
||||
worktrees = getDataForWorktrees()
|
||||
|
||||
updateRepoFromRemote()
|
||||
|
||||
print("Repo is updated from origin")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
|
||||
developmentWorktree = findWorktree(WORKTREE_DEVELOPMENT)
|
||||
|
||||
developmentWorktree.newRevision = getRevisionForRef(developmentWorktree.branch)
|
||||
developmentWorktree.newVersion = getVersion(developmentWorktree.revision)
|
||||
|
||||
print("DEVELOPMENT (" + developmentWorktree.path + ") is on branch " + developmentWorktree.branch)
|
||||
print(developmentWorktree.revision + " = " + developmentWorktree.branch + " (" + developmentWorktree.version + ")")
|
||||
print(developmentWorktree.newRevision + " = origin/" + developmentWorktree.branch + " (" + developmentWorktree.newVersion + ")")
|
||||
|
||||
if developmentWorktree.revision != developmentWorktree.newRevision:
|
||||
print("-> DEVELOPMENT (" + developmentWorktree.path + ") will be UPDATED")
|
||||
print("----------------------------------------------")
|
||||
|
||||
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
|
||||
cleanWorktree(developmentWorktree.path)
|
||||
|
||||
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch + " and cleaned")
|
||||
|
||||
updateAppInWorktree(developmentWorktree.path)
|
||||
updateAppVersionInWorktree(developmentWorktree.path)
|
||||
|
||||
print("MapGuesser is updated in " + developmentWorktree.path)
|
||||
elif developmentWorktree.version != developmentWorktree.newVersion:
|
||||
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
|
||||
|
||||
updateAppVersionInWorktree(developmentWorktree.path)
|
||||
|
||||
print("MapGuesser version is updated in " + developmentWorktree.path)
|
||||
else:
|
||||
print("-> DEVELOPMENT (" + developmentWorktree.path + ") WON'T be updated")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
|
||||
productionWorktree = findWorktree(WORKTREE_PRODUCTION)
|
||||
|
||||
productionWorktree.newVersion = getLatestReleaseTag()
|
||||
productionWorktree.newRevision = getRevisionForRef(productionWorktree.newVersion)
|
||||
|
||||
print("PRODUCTION (" + productionWorktree.path + ")")
|
||||
print(productionWorktree.revision + " = " + productionWorktree.version)
|
||||
print(productionWorktree.newRevision + " = " + productionWorktree.newVersion)
|
||||
|
||||
if productionWorktree.revision != productionWorktree.newRevision:
|
||||
print("-> PRODUCTION (" + productionWorktree.path + ") will be UPDATED")
|
||||
|
||||
checkoutWorktree(productionWorktree.path, productionWorktree.newRevision)
|
||||
cleanWorktree(productionWorktree.path)
|
||||
|
||||
print(productionWorktree.path + " is checked out to " + productionWorktree.newRevision + " and cleaned")
|
||||
|
||||
updateAppInWorktree(productionWorktree.path)
|
||||
updateAppVersionInWorktree(productionWorktree.path)
|
||||
|
||||
print("MapGuesser is updated in " + productionWorktree.path)
|
||||
else:
|
||||
print("-> PRODUCTION (" + productionWorktree.path + ") WON'T be updated")
|
||||
|
||||
print("----------------------------------------------")
|
||||
print("----------------------------------------------")
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
BRANCH_NAME=$(git symbolic-ref --short HEAD)
|
||||
BRANCH_PATTERN="^(bugfix|feature|hotfix)\/([A-Z]+-[0-9]+).*"
|
||||
|
||||
if [[ "${BRANCH_NAME}" =~ $BRANCH_PATTERN ]]; then
|
||||
TICKET_ID=$(echo $BRANCH_NAME | sed -E "s@$BRANCH_PATTERN@\\2@")
|
||||
|
||||
COMMIT_MESSAGE=$(head -n 1 $1)
|
||||
COMMIT_MESSAGE_REGEX="^$TICKET_ID .*"
|
||||
|
||||
if [[ ! "${COMMIT_MESSAGE}" =~ $COMMIT_MESSAGE_REGEX ]]; then
|
||||
sed -i.bak -e "1s/^/$TICKET_ID /" $1
|
||||
fi
|
||||
fi
|
||||
@@ -15,6 +15,9 @@ echo "Installing Yarn packages..."
|
||||
echo "Installing MapGuesser DB..."
|
||||
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./mapg migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
|
||||
|
||||
. ${ROOT_DIR}/.env
|
||||
|
||||
cd ${ROOT_DIR}
|
||||
|
||||
echo "Updating version info..."
|
||||
|
||||
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
|
||||
REVISION=$(git rev-parse --short HEAD)
|
||||
REVISION_DATE=$(git show -s --format=%aI HEAD)
|
||||
|
||||
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
|
||||
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
|
||||
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
|
||||
@@ -10,6 +10,9 @@ echo "Installing Composer packages..."
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
echo "Migrating DB..."
|
||||
(cd ${ROOT_DIR} && ./mapg migrate)
|
||||
|
||||
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
|
||||
echo "Minifying JS, CSS and SVG files..."
|
||||
${ROOT_DIR}/scripts/minify.sh
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Model\User;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('user:add')
|
||||
->setDescription('Adding of user.')
|
||||
->addArgument('email', InputArgument::REQUIRED, 'Email of user')
|
||||
->addArgument('password', InputArgument::REQUIRED, 'Password of user')
|
||||
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$user = new User([
|
||||
'email' => $input->getArgument('email'),
|
||||
]);
|
||||
|
||||
$user->setPlainPassword($input->getArgument('password'));
|
||||
|
||||
if ($input->hasArgument('type')) {
|
||||
$user->setType($input->getArgument('type'));
|
||||
}
|
||||
|
||||
try {
|
||||
$modify = new Modify(\Container::$dbConnection, 'users');
|
||||
$modify->fill($user->toArray());
|
||||
$modify->save();
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>Adding user failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$output->writeln('<info>User was successfully added!</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php namespace MapGuesser\Cli;
|
||||
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class DatabaseMigration extends Command
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('migrate')
|
||||
->setDescription('Migration of database changes.');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$db = \Container::$dbConnection;
|
||||
|
||||
$db->startTransaction();
|
||||
|
||||
$success = [];
|
||||
try {
|
||||
foreach ($this->readDir('structure') as $file) {
|
||||
$db->multiQuery(file_get_contents($file));
|
||||
|
||||
$success[] = $this->saveToDB($file, 'structure');
|
||||
}
|
||||
|
||||
foreach ($this->readDir('data') as $file) {
|
||||
require $file;
|
||||
|
||||
$success[] = $this->saveToDB($file, 'data');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$db->rollback();
|
||||
|
||||
$output->writeln('<error>Migration failed!</error>');
|
||||
$output->writeln('');
|
||||
|
||||
$output->writeln((string) $e);
|
||||
$output->writeln('');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$db->commit();
|
||||
|
||||
$output->writeln('<info>Migration was successful!</info>');
|
||||
$output->writeln('');
|
||||
|
||||
if (count($success) > 0) {
|
||||
foreach ($success as $migration) {
|
||||
$output->writeln($migration);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function readDir(string $type): array
|
||||
{
|
||||
$done = [];
|
||||
|
||||
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
|
||||
AND table_name = \'migrations\';')
|
||||
->fetch(IResultSet::FETCH_NUM)[0];
|
||||
|
||||
if ($migrationTableExists != 0) {
|
||||
$select = new Select(\Container::$dbConnection, 'migrations');
|
||||
$select->columns(['migration']);
|
||||
$select->where('type', '=', $type);
|
||||
$select->orderBy('migration');
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$done[] = $migration['migration'];
|
||||
}
|
||||
}
|
||||
|
||||
$path = ROOT . '/database/migrations/' . $type;
|
||||
$dir = opendir($path);
|
||||
|
||||
$files = [];
|
||||
while ($file = readdir($dir)) {
|
||||
$filePath = $path . '/' . $file;
|
||||
|
||||
if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[] = $filePath;
|
||||
}
|
||||
|
||||
natsort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function saveToDB(string $file, string $type): string
|
||||
{
|
||||
$baseName = pathinfo($file, PATHINFO_FILENAME);
|
||||
|
||||
$modify = new Modify(\Container::$dbConnection, 'migrations');
|
||||
$modify->set('migration', $baseName);
|
||||
$modify->set('type', $type);
|
||||
$modify->save();
|
||||
|
||||
return $baseName . ' (' . $type . ')';
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,54 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
|
||||
class GameController
|
||||
{
|
||||
public function getGame(array $parameters): IContent
|
||||
private IRequest $request;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
$this->request = $request;
|
||||
$this->mapRepository = new MapRepository();
|
||||
}
|
||||
|
||||
public function getGame(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$data = $this->prepareGame($mapId);
|
||||
return new HtmlContent('game', $data);
|
||||
}
|
||||
|
||||
public function getGameJson(array $parameters): IContent
|
||||
public function getGameJson(): IContent
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$data = $this->prepareGame($mapId);
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
private function prepareGame(int $mapId)
|
||||
{
|
||||
$bounds = $this->getMapBounds($mapId);
|
||||
|
||||
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
|
||||
$_SESSION['state'] = [
|
||||
'mapId' => $mapId,
|
||||
'area' => $bounds->calculateApproximateArea(),
|
||||
'rounds' => []
|
||||
];
|
||||
}
|
||||
|
||||
return ['mapId' => $mapId, 'bounds' => $bounds->toArray()];
|
||||
}
|
||||
|
||||
private function getMapBounds(int $mapId): Bounds
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
$select->whereId($mapId);
|
||||
|
||||
$map = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
|
||||
return $bounds;
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$session->set('state', [
|
||||
'mapId' => $mapId,
|
||||
'area' => $bounds->calculateApproximateArea(),
|
||||
'rounds' => []
|
||||
]);
|
||||
}
|
||||
|
||||
return ['mapId' => $mapId, 'mapName' => $map['name'], 'bounds' => $bounds->toArray()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
|
||||
class GameFlowController
|
||||
{
|
||||
const NUMBER_OF_ROUNDS = 5;
|
||||
const MAX_SCORE = 1000;
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
}
|
||||
|
||||
public function getNewPlace(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'no_session_found'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
if (count($state['rounds']) === 0) {
|
||||
$place = $this->placeRepository->getForMapWithValidPano($mapId);
|
||||
$state['rounds'][] = $place;
|
||||
$session->set('state', $state);
|
||||
|
||||
$data = ['panoId' => $place['panoId']];
|
||||
} else {
|
||||
$rounds = count($state['rounds']);
|
||||
$last = $state['rounds'][$rounds - 1];
|
||||
|
||||
$history = [];
|
||||
for ($i = 0; $i < $rounds - 1; ++$i) {
|
||||
$round = $state['rounds'][$i];
|
||||
$history[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'guessPosition' => $round['guessPosition']->toArray(),
|
||||
'distance' => $round['distance'],
|
||||
'score' => $round['score']
|
||||
];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'history' => $history,
|
||||
'panoId' => $last['panoId']
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
public function evaluateGuess(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$session = $this->request->session();
|
||||
|
||||
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'no_session_found'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
$last = $state['rounds'][count($state['rounds']) - 1];
|
||||
|
||||
$position = $last['position'];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
|
||||
$distance = $this->calculateDistance($position, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $state['area']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
$last['distance'] = $distance;
|
||||
$last['score'] = $score;
|
||||
$state['rounds'][count($state['rounds']) - 1] = $last;
|
||||
|
||||
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
|
||||
$exclude = [];
|
||||
|
||||
foreach ($state['rounds'] as $round) {
|
||||
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
|
||||
}
|
||||
|
||||
$place = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
|
||||
$state['rounds'][] = $place;
|
||||
$session->set('state', $state);
|
||||
|
||||
$panoId = $place['panoId'];
|
||||
} else {
|
||||
$state['rounds'] = [];
|
||||
$session->set('state', $state);
|
||||
|
||||
$panoId = null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'result' => [
|
||||
'position' => $position->toArray(),
|
||||
'distance' => $distance,
|
||||
'score' => $score
|
||||
],
|
||||
'panoId' => $panoId
|
||||
];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
private function calculateDistance(Position $realPosition, Position $guessPosition): float
|
||||
{
|
||||
return $realPosition->calculateDistanceTo($guessPosition);
|
||||
}
|
||||
|
||||
private function calculateScore(float $distance, float $area): int
|
||||
{
|
||||
$goodness = 1.0 - ($distance / sqrt($area));
|
||||
|
||||
return (int) round(pow(static::MAX_SCORE, $goodness));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Interfaces\Response\IRedirect;
|
||||
use MapGuesser\Model\User;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Response\Redirect;
|
||||
|
||||
class LoginController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getLoginForm()
|
||||
{
|
||||
$session = $this->request->session();
|
||||
|
||||
if ($session->get('user')) {
|
||||
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
return new HtmlContent('login', $data);
|
||||
}
|
||||
|
||||
public function login(): IContent
|
||||
{
|
||||
$session = $this->request->session();
|
||||
|
||||
if ($session->get('user')) {
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
$select = new Select(\Container::$dbConnection, 'users');
|
||||
$select->columns(User::getFields());
|
||||
$select->where('email', '=', $this->request->post('email'));
|
||||
|
||||
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
if ($userData === null) {
|
||||
$data = ['error' => 'user_not_found'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
$user = new User($userData);
|
||||
|
||||
if (!$user->checkPassword($this->request->post('password'))) {
|
||||
$data = ['error' => 'password_not_match'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
$session->set('user', $user);
|
||||
|
||||
$data = ['success' => true];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
public function logout(): IRedirect
|
||||
{
|
||||
$this->request->session()->delete('user');
|
||||
|
||||
return new Redirect([\Container::$routeCollection->getRoute('login'), []], IRedirect::TEMPORARY);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,203 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Authorization\ISecured;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Repository\MapRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
|
||||
class MapAdminController
|
||||
class MapAdminController implements ISecured
|
||||
{
|
||||
public function getMaps(): IContent {
|
||||
//TODO
|
||||
private static string $unnamedMapName = '[unnamed map]';
|
||||
|
||||
return new HtmlContent('admin/maps');
|
||||
private IRequest $request;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->mapRepository = new MapRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
}
|
||||
|
||||
public function getMapEditor(array $parameters): IContent {
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
public function authorize(): bool
|
||||
{
|
||||
$user = $this->request->user();
|
||||
|
||||
$bounds = $this->getMapBounds($mapId);
|
||||
return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
|
||||
}
|
||||
|
||||
$places = $this->getPlaces($mapId);
|
||||
public function getMapEditor(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
$data = ['mapId' => $mapId, 'bounds' => $bounds->toArray(), 'places' => &$places];
|
||||
if ($mapId) {
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
$places = $this->getPlaces($mapId);
|
||||
} else {
|
||||
$map = [
|
||||
'name' => self::$unnamedMapName,
|
||||
'description' => ''
|
||||
];
|
||||
$bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
|
||||
$places = [];
|
||||
}
|
||||
|
||||
$data = ['mapId' => $mapId, 'mapName' => $map['name'], 'mapDescription' => str_replace('<br>', "\n", $map['description']), 'bounds' => $bounds->toArray(), 'places' => &$places];
|
||||
return new HtmlContent('admin/map_editor', $data);
|
||||
}
|
||||
|
||||
private function getMapBounds(int $mapId): Bounds
|
||||
public function getPlace(): IContent
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
$select->whereId($mapId);
|
||||
$placeId = (int) $this->request->query('placeId');
|
||||
|
||||
$map = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
$placeData = $this->placeRepository->getById($placeId);
|
||||
|
||||
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
|
||||
$data = ['panoId' => $placeData['panoId']];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
public function saveMap(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
|
||||
if (!$mapId) {
|
||||
$mapId = $this->addNewMap();
|
||||
}
|
||||
|
||||
if (isset($_POST['added'])) {
|
||||
$addedIds = [];
|
||||
foreach ($_POST['added'] as $placeRaw) {
|
||||
$placeRaw = json_decode($placeRaw, true);
|
||||
|
||||
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $this->placeRepository->addToMap($mapId, [
|
||||
'lat' => (float) $placeRaw['lat'],
|
||||
'lng' => (float) $placeRaw['lng'],
|
||||
'pano_id_cached_timestamp' => $placeRaw['panoId'] === -1 ? (new DateTime('-1 day'))->format('Y-m-d H:i:s') : null
|
||||
])];
|
||||
}
|
||||
} else {
|
||||
$addedIds = [];
|
||||
}
|
||||
|
||||
if (isset($_POST['edited'])) {
|
||||
foreach ($_POST['edited'] as $placeRaw) {
|
||||
$placeRaw = json_decode($placeRaw, true);
|
||||
|
||||
$this->placeRepository->modify((int) $placeRaw['id'], [
|
||||
'lat' => (float) $placeRaw['lat'],
|
||||
'lng' => (float) $placeRaw['lng']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['deleted'])) {
|
||||
foreach ($_POST['deleted'] as $placeRaw) {
|
||||
$placeRaw = json_decode($placeRaw, true);
|
||||
|
||||
$this->placeRepository->delete($placeRaw['id']);
|
||||
}
|
||||
}
|
||||
|
||||
$mapBounds = $this->calculateMapBounds($mapId);
|
||||
|
||||
$map = [
|
||||
'bound_south_lat' => $mapBounds->getSouthLat(),
|
||||
'bound_west_lng' => $mapBounds->getWestLng(),
|
||||
'bound_north_lat' => $mapBounds->getNorthLat(),
|
||||
'bound_east_lng' => $mapBounds->getEastLng()
|
||||
];
|
||||
|
||||
if (isset($_POST['name'])) {
|
||||
$map['name'] = $_POST['name'] ? $_POST['name'] : self::$unnamedMapName;
|
||||
}
|
||||
if (isset($_POST['description'])) {
|
||||
$map['description'] = str_replace(["\n", "\r\n"], '<br>', $_POST['description']);
|
||||
}
|
||||
|
||||
$this->saveMapData($mapId, $map);
|
||||
|
||||
$data = ['mapId' => $mapId, 'added' => $addedIds];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
private function calculateMapBounds(int $mapId): Bounds
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['lat', 'lng']);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
$bounds = new Bounds();
|
||||
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
$bounds->extend(new Position($place['lat'], $place['lng']));
|
||||
}
|
||||
|
||||
return $bounds;
|
||||
}
|
||||
|
||||
private function addNewMap(): int
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'maps');
|
||||
$modify->fill([
|
||||
'name' => self::$unnamedMapName,
|
||||
'description' => '',
|
||||
'bound_south_lat' => 0.0,
|
||||
'bound_west_lng' => 0.0,
|
||||
'bound_north_lat' => 0.0,
|
||||
'bound_east_lng' => 0.0
|
||||
]);
|
||||
$modify->save();
|
||||
|
||||
return $modify->getId();
|
||||
}
|
||||
|
||||
private function saveMapData(int $mapId, array $map): void
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'maps');
|
||||
$modify->setId($mapId);
|
||||
$modify->fill($map);
|
||||
$modify->save();
|
||||
}
|
||||
|
||||
private function &getPlaces(int $mapId): array
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng']);
|
||||
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
$select->orderBy('lng');
|
||||
//$select->limit(100);
|
||||
|
||||
$places = $select->execute()->fetchAll(IResultSet::FETCH_ASSOC);
|
||||
$result = $select->execute();
|
||||
|
||||
$places = [];
|
||||
|
||||
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
|
||||
//$panoId = ???
|
||||
//$pov = ???
|
||||
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
|
||||
|
||||
$places[$place['id']] = [
|
||||
'id' => $place['id'],
|
||||
'lat' => $place['lat'],
|
||||
'lng' => $place['lng'],
|
||||
'panoId' => null,
|
||||
'pov' => ['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
|
||||
'noPano' => $noPano
|
||||
];
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Database\RawExpression;
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Util\Geo\Bounds;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
|
||||
class MapsController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getMaps(): IContent
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
@@ -23,6 +32,7 @@ class MapsController
|
||||
new RawExpression('COUNT(places.id) AS num_places')
|
||||
]);
|
||||
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
|
||||
$select->groupBy(['maps', 'id']);
|
||||
$select->orderBy('name');
|
||||
|
||||
$result = $select->execute();
|
||||
@@ -36,16 +46,25 @@ class MapsController
|
||||
$maps[] = $map;
|
||||
}
|
||||
|
||||
$data = ['maps' => $maps];
|
||||
$user = $this->request->user();
|
||||
$data = ['maps' => $maps, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)];
|
||||
return new HtmlContent('maps', $data);
|
||||
}
|
||||
|
||||
private function formatMapAreaForHuman(float $area): array
|
||||
{
|
||||
if ($area < 100000.0) {
|
||||
if ($area < 100.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area, 0);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 100000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area, -2);
|
||||
$unit = 'm';
|
||||
} elseif ($area < 1000000.0) {
|
||||
$digits = 2;
|
||||
$rounded = round($area / 1000000.0, 2);
|
||||
$unit = 'km';
|
||||
} elseif ($area < 100000000.0) {
|
||||
$digits = 0;
|
||||
$rounded = round($area / 1000000.0, 0);
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
|
||||
class PositionController
|
||||
{
|
||||
const NUMBER_OF_ROUNDS = 5;
|
||||
const MAX_SCORE = 1000;
|
||||
|
||||
public function getPosition(array $parameters): IContent
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
|
||||
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'No valid session found!'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
if (count($_SESSION['state']['rounds']) === 0) {
|
||||
$newPosition = $this->getNewPosition($mapId);
|
||||
$_SESSION['state']['rounds'][] = $newPosition;
|
||||
|
||||
$data = ['panoId' => $newPosition['panoId']];
|
||||
} else {
|
||||
$rounds = count($_SESSION['state']['rounds']);
|
||||
$last = $_SESSION['state']['rounds'][$rounds - 1];
|
||||
|
||||
$history = [];
|
||||
for ($i = 0; $i < $rounds - 1; ++$i) {
|
||||
$round = $_SESSION['state']['rounds'][$i];
|
||||
$history[] = [
|
||||
'position' => $round['position']->toArray(),
|
||||
'guessPosition' => $round['guessPosition']->toArray(),
|
||||
'distance' => $round['distance'],
|
||||
'score' => $round['score']
|
||||
];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'history' => $history,
|
||||
'panoId' => $last['panoId']
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
public function evaluateGuess(array $parameters): IContent
|
||||
{
|
||||
$mapId = (int) $parameters['mapId'];
|
||||
|
||||
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
|
||||
$data = ['error' => 'No valid session found!'];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
$last = &$_SESSION['state']['rounds'][count($_SESSION['state']['rounds']) - 1];
|
||||
|
||||
$position = $last['position'];
|
||||
$guessPosition = new Position((float) $_POST['lat'], (float) $_POST['lng']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
|
||||
$distance = $this->calculateDistance($position, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $_SESSION['state']['area']);
|
||||
|
||||
$last['distance'] = $distance;
|
||||
$last['score'] = $score;
|
||||
|
||||
if (count($_SESSION['state']['rounds']) < static::NUMBER_OF_ROUNDS) {
|
||||
$exclude = [];
|
||||
|
||||
foreach ($_SESSION['state']['rounds'] as $round) {
|
||||
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
|
||||
}
|
||||
|
||||
$newPosition = $this->getNewPosition($mapId, $exclude);
|
||||
$_SESSION['state']['rounds'][] = $newPosition;
|
||||
|
||||
$panoId = $newPosition['panoId'];
|
||||
} else {
|
||||
$_SESSION['state']['rounds'] = [];
|
||||
|
||||
$panoId = null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'result' => [
|
||||
'position' => $position->toArray(),
|
||||
'distance' => $distance,
|
||||
'score' => $score
|
||||
],
|
||||
'panoId' => $panoId
|
||||
];
|
||||
return new JsonContent($data);
|
||||
}
|
||||
|
||||
private function getNewPosition(int $mapId, array $exclude = []): array
|
||||
{
|
||||
$placesWithoutPano = [];
|
||||
|
||||
do {
|
||||
$place = $this->selectNewPlace($mapId, $exclude);
|
||||
$position = new Position($place['lat'], $place['lng']);
|
||||
$panoId = $this->getPanorama($position);
|
||||
|
||||
if ($panoId === null) {
|
||||
$placesWithoutPano[] = $place['id'];
|
||||
}
|
||||
} while ($panoId === null);
|
||||
|
||||
return [
|
||||
'placesWithoutPano' => $placesWithoutPano,
|
||||
'placeId' => $place['id'],
|
||||
'position' => $position,
|
||||
'panoId' => $panoId
|
||||
];
|
||||
}
|
||||
|
||||
private function selectNewPlace(int $mapId, array $exclude): array
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng']);
|
||||
$select->where('id', 'NOT IN', $exclude);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
|
||||
$numberOfPlaces = $select->count();// TODO: what if 0
|
||||
$randomOffset = random_int(0, $numberOfPlaces - 1);
|
||||
|
||||
$select->orderBy('id');
|
||||
$select->limit(1, $randomOffset);
|
||||
|
||||
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
return $place;
|
||||
}
|
||||
|
||||
private function getPanorama(Position $position): ?string
|
||||
{
|
||||
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
|
||||
$request->setQuery([
|
||||
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
|
||||
'location' => $position->getLat() . ',' . $position->getLng(),
|
||||
'source' => 'outdoor'
|
||||
]);
|
||||
|
||||
$response = $request->send();
|
||||
|
||||
$panoData = json_decode($response->getBody(), true);
|
||||
|
||||
if ($panoData['status'] !== 'OK') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $panoData['pano_id'];
|
||||
}
|
||||
|
||||
private function calculateDistance(Position $realPosition, Position $guessPosition): float
|
||||
{
|
||||
return $realPosition->calculateDistanceTo($guessPosition);
|
||||
}
|
||||
|
||||
private function calculateScore(float $distance, float $area): int
|
||||
{
|
||||
$goodness = 1.0 - ($distance / sqrt($area));
|
||||
|
||||
return (int) round(pow(static::MAX_SCORE, $goodness));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class Modify
|
||||
|
||||
private array $original = [];
|
||||
|
||||
private bool $autoIncrement = false;
|
||||
private bool $autoIncrement = true;
|
||||
|
||||
public function __construct(IConnection $connection, string $table)
|
||||
{
|
||||
@@ -59,6 +59,11 @@ class Modify
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->attributes[$this->idName];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
if (isset($this->attributes[$this->idName])) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php namespace MapGuesser\Interfaces\Authentication;
|
||||
|
||||
interface IUser
|
||||
{
|
||||
const PERMISSION_NORMAL = 0;
|
||||
|
||||
const PERMISSION_ADMIN = 1;
|
||||
|
||||
public function hasPermission(int $permission): bool;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php namespace MapGuesser\Interfaces\Authorization;
|
||||
|
||||
interface ISecured
|
||||
{
|
||||
public function authorize(): bool;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php namespace MapGuesser\Interfaces\Request;
|
||||
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
|
||||
interface IRequest
|
||||
{
|
||||
public function query(string $key);
|
||||
|
||||
public function post(string $key);
|
||||
|
||||
public function session(): ISession;
|
||||
|
||||
public function user(): ?IUser;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php namespace MapGuesser\Interfaces\Request;
|
||||
|
||||
interface ISession
|
||||
{
|
||||
public function has(string $key): bool;
|
||||
|
||||
public function get(string $key);
|
||||
|
||||
public function set(string $key, $value): void;
|
||||
|
||||
public function delete(string $key): void;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php namespace MapGuesser\Model;
|
||||
|
||||
abstract class BaseModel
|
||||
{
|
||||
protected static array $fields;
|
||||
|
||||
protected $id = null;
|
||||
|
||||
public static function getFields(): array
|
||||
{
|
||||
return array_merge(['id'], static::$fields);
|
||||
}
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$this->$method($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setId($id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
function toArray(): array
|
||||
{
|
||||
$array = [];
|
||||
|
||||
foreach (self::getFields() as $key) {
|
||||
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$array[$key] = $this->$method();
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php namespace MapGuesser\Model;
|
||||
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
|
||||
class User extends BaseModel implements IUser
|
||||
{
|
||||
private static array $types = ['user', 'admin'];
|
||||
|
||||
protected static array $fields = ['email', 'password', 'type'];
|
||||
|
||||
private string $email;
|
||||
|
||||
private string $password;
|
||||
|
||||
private string $type = 'user';
|
||||
|
||||
public function setEmail(string $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function setPassword(string $hashedPassword): void
|
||||
{
|
||||
$this->password = $hashedPassword;
|
||||
}
|
||||
|
||||
public function setPlainPassword(string $plainPassword): void
|
||||
{
|
||||
$this->password = password_hash($plainPassword, PASSWORD_BCRYPT);
|
||||
}
|
||||
|
||||
public function setType(string $type): void
|
||||
{
|
||||
if (in_array($type, self::$types)) {
|
||||
$this->type = $type;
|
||||
}
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function hasPermission(int $permission): bool
|
||||
{
|
||||
switch ($permission) {
|
||||
case IUser::PERMISSION_NORMAL:
|
||||
return true;
|
||||
break;
|
||||
case IUser::PERMISSION_ADMIN:
|
||||
return $this->type === 'admin';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPassword(string $password): bool
|
||||
{
|
||||
return password_verify($password, $this->password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
|
||||
class MapRepository
|
||||
{
|
||||
public function getById(int $mapId)
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'maps');
|
||||
$select->columns(['id', 'name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
|
||||
$select->whereId($mapId);
|
||||
|
||||
return $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php namespace MapGuesser\Repository;
|
||||
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Http\Request;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use DateTime;
|
||||
use DateInterval;
|
||||
|
||||
class PlaceRepository
|
||||
{
|
||||
public function getById(int $placeId)
|
||||
{
|
||||
$place = $this->selectFromDbById($placeId);
|
||||
|
||||
$panoId = $this->requestPanoId($place);
|
||||
|
||||
$position = new Position($place['lat'], $place['lng']);
|
||||
|
||||
return [
|
||||
'position' => $position,
|
||||
'panoId' => $panoId
|
||||
];
|
||||
}
|
||||
|
||||
public function getForMapWithValidPano(int $mapId, array $exclude = []): array
|
||||
{
|
||||
$placesWithoutPano = [];
|
||||
|
||||
do {
|
||||
$place = $this->selectFromDbForMap($mapId, $exclude);
|
||||
|
||||
$panoId = $this->requestPanoId($place);
|
||||
|
||||
if ($panoId === null) {
|
||||
$placesWithoutPano[] = $exclude[] = $place['id'];
|
||||
}
|
||||
} while ($panoId === null);
|
||||
|
||||
$position = new Position($place['lat'], $place['lng']);
|
||||
|
||||
return [
|
||||
'placesWithoutPano' => $placesWithoutPano,
|
||||
'placeId' => $place['id'],
|
||||
'position' => $position,
|
||||
'panoId' => $panoId
|
||||
];
|
||||
}
|
||||
|
||||
public function addToMap(int $mapId, array $place): int
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'places');
|
||||
$modify->set('map_id', $mapId);
|
||||
$modify->fill($place);
|
||||
$modify->save();
|
||||
|
||||
return $modify->getId();
|
||||
}
|
||||
|
||||
public function modify(int $id, array $place): void
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'places');
|
||||
$modify->setId($id);
|
||||
$modify->set('pano_id_cached', null);
|
||||
$modify->set('pano_id_cached_timestamp', null);
|
||||
$modify->fill($place);
|
||||
$modify->save();
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'places');
|
||||
$modify->setId($id);
|
||||
$modify->delete();
|
||||
}
|
||||
|
||||
private function selectFromDbById(int $placeId): array
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
|
||||
$select->whereId($placeId);
|
||||
|
||||
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
return $place;
|
||||
}
|
||||
|
||||
private function selectFromDbForMap(int $mapId, array $exclude): array
|
||||
{
|
||||
$select = new Select(\Container::$dbConnection, 'places');
|
||||
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
|
||||
$select->where('id', 'NOT IN', $exclude);
|
||||
$select->where('map_id', '=', $mapId);
|
||||
|
||||
$numberOfPlaces = $select->count(); // TODO: what if 0
|
||||
$randomOffset = random_int(0, $numberOfPlaces - 1);
|
||||
|
||||
$select->orderBy('id');
|
||||
$select->limit(1, $randomOffset);
|
||||
|
||||
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
|
||||
|
||||
return $place;
|
||||
}
|
||||
|
||||
private function requestPanoId(array $place, bool $canBeIndoor = false): ?string
|
||||
{
|
||||
if (
|
||||
$place['pano_id_cached_timestamp'] &&
|
||||
(new DateTime($place['pano_id_cached_timestamp']))->add(new DateInterval('P1D')) > new DateTime()
|
||||
) {
|
||||
return $place['pano_id_cached'];
|
||||
}
|
||||
|
||||
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
|
||||
$request->setQuery([
|
||||
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
|
||||
'location' => $place['lat'] . ',' . $place['lng'],
|
||||
'source' => $canBeIndoor ? 'default' : 'outdoor'
|
||||
]);
|
||||
|
||||
$response = $request->send();
|
||||
|
||||
$panoData = json_decode($response->getBody(), true);
|
||||
|
||||
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
|
||||
|
||||
// enable indoor panos if no outdoor found
|
||||
if ($panoId === null && !$canBeIndoor) {
|
||||
return $this->requestPanoId($place, true);
|
||||
}
|
||||
|
||||
$this->saveCachedPanoId($place['id'], $panoId);
|
||||
|
||||
return $panoId;
|
||||
}
|
||||
|
||||
private function saveCachedPanoId(int $placeId, ?string $panoId): void
|
||||
{
|
||||
$modify = new Modify(\Container::$dbConnection, 'places');
|
||||
$modify->setId($placeId);
|
||||
$modify->set('pano_id_cached', $panoId);
|
||||
$modify->set('pano_id_cached_timestamp', (new DateTime())->format('Y-m-d H:i:s'));
|
||||
$modify->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php namespace MapGuesser\Request;
|
||||
|
||||
use MapGuesser\Interfaces\Authentication\IUser;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Interfaces\Request\ISession;
|
||||
use MapGuesser\Model\User;
|
||||
|
||||
class Request implements IRequest
|
||||
{
|
||||
private array $get;
|
||||
|
||||
private array $routeParams;
|
||||
|
||||
private array $post;
|
||||
|
||||
private Session $session;
|
||||
|
||||
public function __construct(array &$get, array &$routeParams, array &$post, array &$session)
|
||||
{
|
||||
$this->get = &$get;
|
||||
$this->routeParams = &$routeParams;
|
||||
$this->post = &$post;
|
||||
$this->session = new Session($session);
|
||||
}
|
||||
|
||||
public function query($key)
|
||||
{
|
||||
if (isset($this->get[$key])) {
|
||||
return $this->get[$key];
|
||||
}
|
||||
|
||||
if (isset($this->routeParams[$key])) {
|
||||
return $this->routeParams[$key];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function post($key)
|
||||
{
|
||||
if (isset($this->post[$key])) {
|
||||
return $this->post[$key];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function session(): ISession
|
||||
{
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
public function user(): ?IUser
|
||||
{
|
||||
return $this->session->get('user');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php namespace MapGuesser\Request;
|
||||
|
||||
use MapGuesser\Interfaces\Request\ISession;
|
||||
|
||||
class Session implements ISession
|
||||
{
|
||||
private array $data;
|
||||
|
||||
public function __construct(array &$data)
|
||||
{
|
||||
$this->data = &$data;
|
||||
}
|
||||
|
||||
public function has($key): bool
|
||||
{
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
public function get($key)
|
||||
{
|
||||
if (isset($this->data[$key])) {
|
||||
return $this->data[$key];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function set($key, $value): void
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
public function delete($key): void
|
||||
{
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ class Route
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getHandler(): array
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function generateLink(array $parameters = []): string
|
||||
{
|
||||
$link = [];
|
||||
@@ -51,14 +56,6 @@ class Route
|
||||
return implode('/', $link) . $query;
|
||||
}
|
||||
|
||||
public function callController(array $parameters)
|
||||
{
|
||||
$controllerName = $this->handler[0];
|
||||
$controller = new $controllerName();
|
||||
|
||||
return call_user_func([$controller, $this->handler[1]], $parameters);
|
||||
}
|
||||
|
||||
public function testAgainst(array $path): ?array
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
@@ -59,6 +59,26 @@ class Bounds
|
||||
}
|
||||
}
|
||||
|
||||
public function getSouthLat(): float
|
||||
{
|
||||
return $this->southLat;
|
||||
}
|
||||
|
||||
public function getWestLng(): float
|
||||
{
|
||||
return $this->westLng;
|
||||
}
|
||||
|
||||
public function getNorthLat(): float
|
||||
{
|
||||
return $this->northLat;
|
||||
}
|
||||
|
||||
public function getEastLng(): float
|
||||
{
|
||||
return $this->eastLng;
|
||||
}
|
||||
|
||||
public function calculateApproximateArea(): float
|
||||
{
|
||||
$dLat = $this->northLat - $this->southLat;
|
||||
|
||||
+66
-10
@@ -1,10 +1,68 @@
|
||||
<?php $cssFiles = ['/static/node_modules/leaflet/dist/leaflet.css', '/static/css/map_editor.css']; ?>
|
||||
<?php
|
||||
$cssFiles = [
|
||||
'node_modules/leaflet/dist/leaflet.css',
|
||||
'css/map_editor.css',
|
||||
'node_modules/leaflet.markercluster/dist/MarkerCluster.css',
|
||||
'node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css'
|
||||
];
|
||||
$jsFiles = [
|
||||
'node_modules/leaflet/dist/leaflet.js',
|
||||
'node_modules/leaflet.markercluster/dist/leaflet.markercluster.js',
|
||||
'https://maps.googleapis.com/maps/api/js?key=' . $_ENV['GOOGLE_MAPS_JS_API_KEY'],
|
||||
'js/map_editor.js',
|
||||
];
|
||||
?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div class="header small">
|
||||
<div class="grid">
|
||||
<h1>
|
||||
<a href="/maps" title="Back to playable maps">
|
||||
<img class="inline" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>">
|
||||
<span>MapGuesser</span>
|
||||
</a>
|
||||
</h1>
|
||||
<p>
|
||||
<span class="bold"><a href="javascript:;" id="mapName"><?= $mapName ?></a></span><!--
|
||||
--><span><!--
|
||||
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
|
||||
--><svg class="inline" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M8 3.5a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5H4a.5.5 0 0 1 0-1h3.5V4a.5.5 0 0 1 .5-.5z"/>
|
||||
<path fill-rule="evenodd" d="M7.5 8a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1H8.5V12a.5.5 0 0 1-1 0V8z"/>
|
||||
<path fill-rule="evenodd" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
|
||||
</svg>
|
||||
<span id="added" class="bold">0</span><!--
|
||||
--></span><!--
|
||||
--><span><!--
|
||||
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
|
||||
--><svg class="inline" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.502 1.94a.5.5 0 0 1 0 .706a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456l-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/> <path fill-rule="evenodd" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
|
||||
</svg>
|
||||
<span id="edited" class="bold">0</span><!--
|
||||
--></span><!--
|
||||
--><span><!--
|
||||
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
|
||||
--><svg class="inline" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M3.5 8a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.5-.5z"/>
|
||||
<path fill-rule="evenodd" d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
|
||||
</svg>
|
||||
<span id="deleted" class="bold">0</span><!--
|
||||
--></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="metadata">
|
||||
<form id="metadataForm">
|
||||
<input class="fullWidth" type="text" name="name" value="<?= $mapName ?>" placeholder="Name of the map">
|
||||
<textarea class="fullWidth marginTop" name="description" rows="4" placeholder="Description of the map"><?= $mapDescription ?></textarea>
|
||||
<div style="text-align: right;">
|
||||
<button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
|
||||
<button class="marginTop" type="submit">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<div id="control">
|
||||
<button id="saveButton" class="fullWidth">Save</button>
|
||||
<a class="button gray fullWidth marginTop" href="/admin/maps" title="Back to maps">Back to maps</a>
|
||||
<button id="saveButton" class="fullWidth" disabled>Save</button>
|
||||
</div>
|
||||
<div id="panorama"></div>
|
||||
<div id="noPano">
|
||||
@@ -12,16 +70,14 @@
|
||||
</div>
|
||||
<div id="placeControl">
|
||||
<button id="applyButton" class="fullWidth">Apply</button>
|
||||
<button id="cancelButton" class="gray fullWidth marginTop">Cancel</button>
|
||||
<button id="closeButton" class="gray fullWidth marginTop">Close</button>
|
||||
<button id="deleteButton" class="red fullWidth marginTop">Delete</button>
|
||||
</div>
|
||||
<script>
|
||||
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
|
||||
var mapId = '<?= $mapId ?>';
|
||||
var tileAttribution = '<?= $_ENV['LEAFLET_TILESERVER_ATTRIBUTION'] ?>';
|
||||
var mapId = <?= $mapId ?>;
|
||||
var mapBounds = <?= json_encode($bounds) ?>;
|
||||
var places = <?= json_encode($places) ?>;
|
||||
var places = <?= json_encode($places, JSON_FORCE_OBJECT) ?>;
|
||||
</script>
|
||||
<script src="/static/node_modules/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>"></script>
|
||||
<script src="/static/js/map_editor.js"></script>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div class="main">
|
||||
<h2>Maps</h2>
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
||||
+16
-6
@@ -1,14 +1,26 @@
|
||||
<?php $cssFiles = ['/static/css/game.css']; ?>
|
||||
<?php
|
||||
$cssFiles = [
|
||||
'css/game.css'
|
||||
];
|
||||
$jsFiles = [
|
||||
'https://maps.googleapis.com/maps/api/js?key=' . $_ENV['GOOGLE_MAPS_JS_API_KEY'],
|
||||
'js/game.js',
|
||||
];
|
||||
?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<div class="header small">
|
||||
<div class="grid">
|
||||
<h1>
|
||||
<a href="/maps" title="Back to playable maps">
|
||||
<img class="inline" src="/static/img/icon.svg">
|
||||
<img class="inline" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>">
|
||||
<span>MapGuesser</span>
|
||||
</a>
|
||||
</h1>
|
||||
<p id="roundInfo">Round: <span id="currentRound" class="mono bold"></span> | Score: <span id="currentScoreSum" class="mono bold"></span></p>
|
||||
<p>
|
||||
<span id="mapName" class="bold"><?= $mapName ?></span><!--
|
||||
--><span>Round <span id="currentRound" class="bold"></span></span><!--
|
||||
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cover"></div>
|
||||
@@ -46,9 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var mapId = '<?= $mapId ?>';
|
||||
var mapId = <?= $mapId ?>;
|
||||
var mapBounds = <?= json_encode($bounds) ?>;
|
||||
</script>
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>"></script>
|
||||
<script src="/static/js/game.js"></script>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
$jsFiles = [
|
||||
'js/login.js',
|
||||
];
|
||||
?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div class="main">
|
||||
<h2>Login</h2>
|
||||
<div class="box">
|
||||
<form id="loginForm" action="/login" method="post">
|
||||
<input class="big fullWidth" type="email" name="email" placeholder="Email address" autofocus>
|
||||
<input class="big fullWidth marginTop" type="password" name="password" placeholder="Password">
|
||||
<p id="loginFormError" class="formError marginTop"></p>
|
||||
<div class="right marginTop">
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php require ROOT . '/views/templates/main_footer.php'; ?>
|
||||
+25
-4
@@ -1,4 +1,8 @@
|
||||
<?php $cssFiles = ['/static/css/maps.css']; ?>
|
||||
<?php
|
||||
$cssFiles = [
|
||||
'css/maps.css'
|
||||
];
|
||||
?>
|
||||
<?php require ROOT . '/views/templates/main_header.php'; ?>
|
||||
<?php require ROOT . '/views/templates/header.php'; ?>
|
||||
<div class="main">
|
||||
@@ -30,11 +34,28 @@
|
||||
</div>
|
||||
<p class="small justify marginTop"><?= $map['description'] ?></p>
|
||||
</div>
|
||||
<a class="button fullWidth" href="game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<?php if ($isAdmin): ?>
|
||||
<div class="buttonContainer">
|
||||
<a class="button fullWidth noRightRadius" href="game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<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" title="Delete map '<?= $map['name'] ?>'">Delete</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<a class="button fullWidth" href="game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (count($maps) < 4): ?>
|
||||
<?php for ($i = 0; $i < 4 - count($maps); ++$i): ?>
|
||||
<?php if ($isAdmin): ?>
|
||||
<div class="mapItem new">
|
||||
<a class="button green fullWidth" href="admin/mapEditor" title="Add new map">
|
||||
Add new map
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mapItem"></div>
|
||||
<?php endif; ?>
|
||||
<?php if (count($maps) < 3): ?>
|
||||
<?php for ($i = 0; $i < 3 - count($maps); ++$i): ?>
|
||||
<div class="mapItem"></div>
|
||||
<?php endfor; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="header">
|
||||
<h1>
|
||||
<a href="/" title="MapGuesser">
|
||||
<img class="inline" src="/static/img/icon.svg">
|
||||
<img class="inline" src="<?= $_ENV['STATIC_ROOT'] ?>/img/icon.svg?rev=<?= REVISION ?>">
|
||||
MapGuesser
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
<script>
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
});
|
||||
const STATIC_ROOT = '<?= $_ENV['STATIC_ROOT'] ?>';
|
||||
const REVISION = '<?= REVISION ?>';
|
||||
</script>
|
||||
<?php if (isset($jsFiles)) : ?>
|
||||
<?php foreach ($jsFiles as $jsFile) : ?>
|
||||
<?php
|
||||
if (!preg_match('/^http(s)?/', $jsFile)) {
|
||||
$jsFile = $_ENV['STATIC_ROOT'] . '/' . $jsFile . '?rev=' . REVISION;
|
||||
}
|
||||
?>
|
||||
<script src="<?= $jsFile ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<script>
|
||||
(function () {
|
||||
var anchors = document.getElementsByTagName('a');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
var a = anchors[i];
|
||||
if (a.href !== 'javascript:;' && a.target !== '_blank') {
|
||||
a.onclick = function () {
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,19 +4,24 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MapGuesser</title>
|
||||
<link href="/static/css/mapguesser.css" rel="stylesheet">
|
||||
<link href="<?= $_ENV['STATIC_ROOT'] ?>/css/mapguesser.css?rev=<?= REVISION ?>" rel="stylesheet">
|
||||
<?php if (isset($cssFiles)) : ?>
|
||||
<?php foreach ($cssFiles as $cssFile) : ?>
|
||||
<?php
|
||||
if (!preg_match('/^http(s)?/', $cssFile)) {
|
||||
$cssFile = $_ENV['STATIC_ROOT'] . '/' . $cssFile . '?rev=' . REVISION;
|
||||
}
|
||||
?>
|
||||
<link href="<?= $cssFile ?>" rel="stylesheet">
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&family=Roboto+Mono:wght@300;500&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/static/img/favicon/192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/img/favicon/96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/img/favicon/32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/img/favicon/16x16.png">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/192x192.png?rev=<?= REVISION ?>">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/96x96.png?rev=<?= REVISION ?>">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/32x32.png?rev=<?= REVISION ?>">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/16x16.png?rev=<?= REVISION ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<img src="/static/img/loading.svg">
|
||||
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/loading.svg?rev=<?= REVISION ?>">
|
||||
</div>
|
||||
Reference in new issue
Block a user