Compare commits

...
Author SHA1 Message Date
bence 2aa3cdc7f3 MAPG-125 re-add select map name when opening modal 2020-06-14 02:53:11 +02:00
bence 5760e31e29 Merged in feature/MAPG-125-ability-to-delete-maps (pull request #108)
Feature/MAPG-125 ability to delete maps
2020-06-14 00:42:31 +00:00
bence 14fd723f26 MAPG-125 add functionality to delete map button 2020-06-14 02:37:49 +02:00
bence a6d5214166 MAPG-125 add deleteMap endpoint 2020-06-14 02:36:23 +02:00
bence d1d9dede9c MAPG-125 use modal instead of custom box in map editor 2020-06-14 02:31:09 +02:00
bence 171064816c MAPG-125 add modal design 2020-06-14 02:30:47 +02:00
bence f8bb7bd092 MAPG-125 rename #cover to #guessCover in game 2020-06-14 02:07:20 +02:00
bence 466f065718 MAPG-125 switch off diff calculation in Modify for better performance
(not sure if needed - we probably won't log)
2020-06-14 02:06:27 +02:00
bence 6d68f247e6 Merged in feature/MAPG-133-csrf-protection (pull request #107)
Feature/MAPG-133 csrf protection
2020-06-13 20:41:49 +00:00
bence e4c7aadc64 MAPG-133 replace XHRs to MapGuesser.httpRequest 2020-06-13 22:39:44 +02:00
bence 25262fb5fe MAPG-133 pass CSRF token to JS
create separate mapguesser.js
2020-06-13 22:39:17 +02:00
bence 8e0c1ce08b MAPG-133 add basic CSRF protection to server side 2020-06-13 22:38:30 +02:00
bence 9e196ebf33 Merged in feature/MAPG-131-store-session-is-database (pull request #106)
Feature/MAPG-131 store session is database
2020-06-13 18:49:26 +00:00
bence 013668e3d6 MAPG-131 split web specific code from main.php to web.php
set the new session handler as session handler
configure session with longer lifetime and with stricter cookie settings
2020-06-13 20:45:36 +02:00
bence dbd16d6cbe MAPG-131 create custom session handler for DB sessions 2020-06-13 20:39:42 +02:00
bence 6a191d5c28 MAPG-131 adapt Modify to be able to handle external ID 2020-06-13 20:39:22 +02:00
bence 39e49724da MAPG-131 add sessions table to DB 2020-06-13 20:38:45 +02:00
bence 6c8a3c6a90 Merged in feature/MAPG-111-calculate-map-area-when-bounds-are-modified (pull request #105)
Feature/MAPG-111 calculate map area when bounds are modified
2020-06-12 19:57:07 +00:00
bence 7159867db0 MAPG-111 query map area from DB
modify calculations to be in square km (instead of m)
2020-06-12 21:55:04 +02:00
bence 4d7c86fd1b MAPG-111 store map area in table 'maps' 2020-06-12 21:54:05 +02:00
bence 9ca97dccb9 Merged in bugfix/MAPG-28-rewrite-missiong-static-asset-paths (pull request #104)
MAPG-28 fix static asset paths (add STATIC_ROOT and REVISION)
2020-06-12 18:16:36 +00:00
bence 2ef67d53f4 MAPG-28 fix static asset paths (add STATIC_ROOT and REVISION) 2020-06-12 20:13:43 +02:00
bence 06a0722899 Merged in feature/MAPG-28-configure-static-root-for-assets (pull request #103)
Feature/MAPG-28 configure static root for assets
2020-06-12 18:01:56 +00:00
bence 4a9cf8a217 MAPG-28 add update-version.sh and adapt deploy py script to that 2020-06-12 20:00:45 +02:00
bence b3caf1fb38 MAPG-28 adapt cssFiles and jsFiles to the new handler 2020-06-12 20:00:45 +02:00
bence 08d4aec48e MAPG-28 add static root and revision to the local JS and CSS files 2020-06-12 20:00:45 +02:00
bence 9c380076fa Merged in feature/MAPG-129-add-git-hooks (pull request #102)
MAPG-129 add prepare-commit-msg hook template
2020-06-12 17:48:37 +00:00
bence 95508ad284 MAPG-129 add prepare-commit-msg hook template 2020-06-12 19:47:58 +02:00
bence 7690714b26 MAPG-28 make it possible to configure static root (eg. for static.example.com) 2020-06-12 19:42:55 +02:00
bence f732dad1b1 MAPG-28 add version and revision info in main.php 2020-06-12 19:39:25 +02:00
bence 49b6f28a9d Merged in bugfix/MAPG-130-fix-loading-animation-position (pull request #101)
MAPG-130 loading animation should be fixed and not absolute
2020-06-11 21:53:47 +00:00
bence 33ec9d3a9c MAPG-130 loading animation should be fixed and not absolute 2020-06-11 23:53:15 +02:00
bence 6fb122e842 Merged in feature/MAPG-127-integrate-markerclustergroup (pull request #100)
Feature/MAPG-127 integrate markerclustergroup
2020-06-11 21:35:49 +00:00
bence 90ccdf6de7 MAPG-127 cluster markers in map_editor 2020-06-11 23:23:28 +02:00
bence a869a55b7e MAPG-127 load markercluster's CSS and JS in map_editor 2020-06-11 23:23:03 +02:00
bence 420ed400a6 MAPG-127 add leaflet.markercluster to yarn dependencies 2020-06-11 23:22:26 +02:00
bence d6f34b916e Merged in hotfix/MAPG-128-saving-a-new-map-does-not-work (pull request #99)
MAPG-128 set all default values when creating a new map
2020-06-11 08:34:52 +00:00
bence 98fc735622 MAPG-128 set all default values when creating a new map 2020-06-11 10:33:59 +02:00
bence 81a3079a1a Merged in feature/MAPG-126-add-attribution-for-leaflet-map (pull request #98)
MAPG-126 add configurable attribution for Leaflet maps
2020-06-10 22:15:02 +00:00
bence a0d1e8778c MAPG-126 add configurable attribution for Leaflet maps 2020-06-11 00:14:30 +02:00
bence 2fc0ab557f Merged in feature/MAPG-119-new-map-creation (pull request #97)
Feature/MAPG-119 new map creation
2020-06-10 21:59:49 +00:00
bence be8f0a1486 MAPG-119 fix distribution of the ids of the newly created places 2020-06-10 23:58:09 +02:00
bence ff201c2e8e MAPG-119 add more area categories 2020-06-10 23:51:09 +02:00
bence 805d91c0c0 MAPG-119 small fixes for game 2020-06-10 23:36:00 +02:00
bence ec7e806277 MAPG-119 'add new map' button and some desing improvements on maps page 2020-06-10 23:36:00 +02:00
bence 947ce6b183 MAPG-119 view and JS adaptations for new map creation 2020-06-10 23:32:38 +02:00
bence 6fc83c9459 MAPG-119 handle the case if no mapId is present when editing map 2020-06-10 23:32:37 +02:00
bence 5237c3f454 MAPG-119 prevent endless loop for place selection 2020-06-10 23:12:30 +02:00
bence 5a1c384c17 MAPG-119 fix maps query 2020-06-10 23:11:55 +02:00
bence 3fb538469b Merged in bugfix/MAPG-121-game-doesnt-work (pull request #96)
MAPG-121 fix wrong condition
2020-06-10 18:35:57 +00:00
bence 04d10663dd MAPG-121 fix wrong condition 2020-06-10 20:34:29 +02:00
bence f11cba4f10 Merged in bugfix/MAPG-120-fix-login-loading (pull request #95)
MAPG-120 don't hide the loading when user will be redirected
2020-06-10 18:23:11 +00:00
bence cfb0513825 Merged in bugfix/MAPG-117-map-selection-page (pull request #94)
MAPG-117 show the edit button only if user is admin
2020-06-10 18:22:51 +00:00
bence ed0e287c94 MAPG-120 don't hide the loading when user will be redirected 2020-06-10 20:22:01 +02:00
bence ad818af543 MAPG-117 show the edit button only if user is admin 2020-06-10 20:20:36 +02:00
bence 2725754504 Merged in bugfix/MAPG-116-improve-loading-animation (pull request #93)
MAPG-116 don't show loading when no real link clicked
2020-06-09 22:41:25 +00:00
bence dc072bd3c0 Merged in bugfix/MAPG-41-create-place-administration (pull request #92)
MAPG-41 fix line ending replace
2020-06-09 22:41:10 +00:00
bence a70d59ed95 MAPG-116 don't show loading when no real link clicked 2020-06-10 00:40:47 +02:00
bence 94cab9f433 MAPG-41 fix line ending replace 2020-06-10 00:33:11 +02:00
bence 4b0057e3ba Merged in feature/MAPG-117-map-selection-page (pull request #91)
MAPG-117 remove admin/maps and place a button to the maps page if admin is logged in
2020-06-09 22:31:40 +00:00
bence 5a450c46db MAPG-117 remove admin/maps and place a button to the maps page if admin is logged in 2020-06-10 00:30:29 +02:00
bence 4af6f07ce1 Merged in feature/MAPG-116-improve-loading-animation (pull request #90)
MAPG-116 attach loading animation to anchors and not to beforeunload
2020-06-09 21:01:07 +00:00
bence 5e28fcb64d MAPG-116 attach loading animation to anchors and not to beforeunload 2020-06-09 23:00:07 +02:00
bence 16bcbaecbe Merged in feature/MAPG-103-map-editor-ui-elements (pull request #88)
Feature/MAPG-103 map editor ui elements
2020-06-09 19:40:33 +00:00
bence fbab712496 MAPG-103 save map metadata 2020-06-09 21:29:51 +02:00
bence 46ef7f3b3b MAPG-103 unify some styles and show map name in the header 2020-06-09 21:29:51 +02:00
bence 5dd877d075 MAPG-103 implement saving the modified map on server side 2020-06-09 21:29:51 +02:00
bence 88e8c5c027 MAPG-103 implement saving on client side + server side skeleton 2020-06-09 21:29:51 +02:00
bence 3fd37b0a78 MAPG-103 add info about the modified places into the header 2020-06-09 21:13:16 +02:00
bence 92fd282184 MAPG-103 reset UI elements in case of error 2020-06-09 21:13:16 +02:00
bence 55a1cd24d6 MAPG-103 make some CSS rules more general 2020-06-09 21:13:16 +02:00
bence bb29bfdfe1 MAPG-103 don't use "position" everywhere 2020-06-09 21:13:16 +02:00
bence 1179914f42 MAPG-103 rename "Core" to "Game" in game.js 2020-06-09 21:10:15 +02:00
bence 85773d47a0 MAPG-103 create controls for place 2020-06-09 21:10:15 +02:00
bence 199374a851 MAPG-103 prepare to send additional data with places 2020-06-09 21:10:15 +02:00
bence 138ff3fe6a MAPG-103 make possible to select a new place on map 2020-06-09 21:10:15 +02:00
bence 1915b252c6 MAPG-103 change map's cursor to crosshair in map editor 2020-06-09 21:10:15 +02:00
bence 2f2a7968a0 Merged in feature/MAPG-115-guard-endpoints-based-on-user-g (pull request #89)
Feature/MAPG-115 guard endpoints based on user g
2020-06-09 19:05:23 +00:00
bence edc2e4111a MAPG-115 add login functionality 2020-06-09 21:04:27 +02:00
bence d93a758cd2 MAPG-115 make possible to add new users via command line 2020-06-09 21:04:27 +02:00
bence e8f9d74283 MAPG-115 adapt existing controllers to the new request classes 2020-06-09 21:04:27 +02:00
bence 00dc78b50c MAPG-115 add base model and User model 2020-06-09 21:04:27 +02:00
bence fe814908c7 MAPG-115 add new classes to store request data (get, post, session, etc.) 2020-06-09 21:04:27 +02:00
bence ecaf8ca9d4 MAPG-115 make possible to secure controllers 2020-06-09 00:57:01 +02:00
bence ed9621d581 MAPG-115 add users table 2020-06-09 00:57:01 +02:00
50 changed files with 2005 additions and 383 deletions

No files matched your search

+2
View File
@@ -6,3 +6,5 @@ DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
STATIC_ROOT=/static
@@ -0,0 +1,24 @@
<?php
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Bounds;
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns(['id', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
$result = $select->execute();
\Container::$dbConnection->startTransaction();
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($map['id']);
$modify->set('area', $bounds->calculateApproximateArea());
$modify->save();
}
\Container::$dbConnection->commit();
@@ -0,0 +1,8 @@
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(100) NOT NULL,
`password` varchar(60) NOT NULL,
`type` enum('user', 'admin') NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
@@ -0,0 +1,4 @@
ALTER TABLE
`maps`
ADD
`area` decimal(13, 4) NOT NULL DEFAULT 0.0;
@@ -0,0 +1,6 @@
CREATE TABLE `sessions` (
`id` varchar(64) NOT NULL,
`data` text NOT NULL,
`updated` timestamp NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+4 -11
View File
@@ -3,25 +3,18 @@
require 'vendor/autoload.php'; require 'vendor/autoload.php';
const ROOT = __DIR__; const ROOT = __DIR__;
const VERSION = '';
const REVISION = '';
const REVISION_DATE = '';
$dotenv = Dotenv\Dotenv::createImmutable(ROOT); $dotenv = Dotenv\Dotenv::createImmutable(ROOT);
$dotenv->load(); $dotenv->load();
if (!empty($_ENV['DEV'])) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
ini_set('display_errors', '0');
}
class Container class Container
{ {
static MapGuesser\Interfaces\Database\IConnection $dbConnection; static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection; static MapGuesser\Routing\RouteCollection $routeCollection;
static \SessionHandlerInterface $sessionHandler;
} }
Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
session_start();
+1
View File
@@ -6,5 +6,6 @@ require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', ''); $app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\DatabaseMigration()); $app->add(new MapGuesser\Cli\DatabaseMigration());
$app->add(new MapGuesser\Cli\AddUserCommand());
$app->run(); $app->run();
+33 -25
View File
@@ -1,8 +1,7 @@
<?php <?php
require '../main.php'; require '../web.php';
// very basic routing
$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; $host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
$method = strtolower($_SERVER['REQUEST_METHOD']); $method = strtolower($_SERVER['REQUEST_METHOD']);
$url = substr($_SERVER['REQUEST_URI'], strlen('/')); $url = substr($_SERVER['REQUEST_URI'], strlen('/'));
@@ -11,34 +10,43 @@ if (($pos = strpos($url, '?')) !== false) {
} }
$url = rawurldecode($url); $url = rawurldecode($url);
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
$routeCollection->get('position-json', '{mapId}/position.json', [MapGuesser\Controller\PositionController::class, 'getPosition']);
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\PositionController::class, 'evaluateGuess']);
});
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.place', 'place.json/{placeId}', [MapGuesser\Controller\MapAdminController::class, 'getPlace']);
});
$match = Container::$routeCollection->match($method, explode('/', $url)); $match = Container::$routeCollection->match($method, explode('/', $url));
if ($match !== null) { if ($match !== null) {
list($route, $params) = $match; list($route, $params) = $match;
$response = $route->callController($params); $request = new MapGuesser\Request\Request($_GET, $params, $_POST, $_SESSION);
if ($response instanceof MapGuesser\Interfaces\Response\IContent) { $handler = $route->getHandler();
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8'); $controller = new $handler[0]($request);
echo $response->render();
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) { if ($controller instanceof MapGuesser\Interfaces\Authorization\ISecured) {
header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode()); $authorized = $controller->authorize();
} else {
$authorized = true;
}
if ($method === 'post' && $request->post('anti_csrf_token') !== $request->session()->get('anti_csrf_token')) {
header('Content-Type: text/html; charset=UTF-8', true, 403);
echo json_encode(['error' => 'no_valid_anti_csrf_token']);
return;
}
if ($authorized) {
$response = call_user_func([$controller, $handler[1]]);
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
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';
+4 -11
View File
@@ -1,20 +1,10 @@
#roundInfo {
line-height: inherit;
text-align: right;
}
#roundInfo p {
font-size: 16px;
line-height: inherit;
}
#panorama { #panorama {
width: 100%; width: 100%;
height: calc(100% - 40px); height: calc(100% - 40px);
z-index: 1; z-index: 1;
} }
#cover { #guessCover {
position: absolute; position: absolute;
top: 40px; top: 40px;
left: 0; left: 0;
@@ -110,6 +100,9 @@
} }
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
#mapName {
display: none;
}
#showGuessButtonContainer { #showGuessButtonContainer {
position: absolute; position: absolute;
left: 20px; left: 20px;
+21 -14
View File
@@ -1,9 +1,13 @@
#map { #map {
width: 100%; width: 100%;
height: calc(100% - 50px); height: calc(100% - 40px);
z-index: 1; z-index: 1;
} }
.leaflet-container {
cursor: crosshair;
}
#panorama { #panorama {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
@@ -24,34 +28,36 @@
#control { #control {
position: absolute; position: absolute;
top: 50px;
right: 10px;
width: 125px; width: 125px;
z-index: 3; z-index: 3;
} }
#placeControl { #placeControl {
position: absolute; position: absolute;
right: 10px;
z-index: 3; z-index: 3;
width: 100px; width: 100px;
visibility: hidden; visibility: hidden;
} }
#deleteButton {
display: none;
}
@media screen and (max-width: 999px) and (min-height: 600px) { @media screen and (max-width: 999px) and (min-height: 600px) {
#map.selected { #map.selected {
height: calc(50% - 25px); height: calc(50% - 20px);
} }
#panorama, #noPano { #panorama, #noPano {
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
height: calc(50% - 25px); height: calc(50% - 20px);
}
#control {
right: 10px;
top: 60px;
} }
#placeControl { #placeControl {
right: 10px; top: calc(50% + 30px);
top: calc(50% + 35px);
} }
} }
@@ -60,17 +66,18 @@
width: 50%; width: 50%;
} }
#panorama, #noPano { #panorama, #noPano {
top: 50px; top: 40px;
bottom: 0; bottom: 0;
right: 0; right: 0;
width: 50%; width: 50%;
} }
#control, #placeControl { #placeControl {
right: 10px; top: 50px;
top: 60px; }
#modified.selected {
right: calc(50% + 10px);
} }
#control.selected { #control.selected {
right: calc(50% + 10px); right: calc(50% + 10px);
top: 60px;
} }
} }
+155 -4
View File
@@ -17,7 +17,7 @@ button::-moz-focus-inner, input::-moz-focus-inner {
border: 0; border: 0;
} }
p, h1, h2, button, a { p, h1, h2, input, textarea, select, button, a {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
} }
@@ -89,12 +89,24 @@ sub {
margin-top: 10px; margin-top: 10px;
} }
.marginLeft {
margin-left: 10px;
}
.marginBottom { .marginBottom {
margin-bottom: 10px; margin-bottom: 10px;
} }
.marginRight {
margin-right: 10px;
}
.right {
text-align: right;
}
svg.inline, img.inline { svg.inline, img.inline {
display: inline-block; display: inline;
width: 1em; width: 1em;
height: 1em; height: 1em;
vertical-align: -0.15em; vertical-align: -0.15em;
@@ -142,6 +154,16 @@ button.fullWidth, a.button.fullWidth {
width: 100%; 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 { button.gray, a.button.gray {
background-color: #808080; background-color: #808080;
} }
@@ -158,6 +180,93 @@ button.red:hover, button.red:focus, a.button.red:hover, a.button.red:focus {
background-color: #7f2929; 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;
}
div.modal {
position: fixed;
top: 100px;
background-color: #ffffff;
border-radius: 3px;
padding: 20px;
box-sizing: border-box;
z-index: 3;
visibility: hidden;
}
#cover {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 2;
visibility: hidden;
}
p.formError {
color: #7f2929;
font-weight: 500;
display: none;
}
div.header { div.header {
background-color: #333333; background-color: #333333;
height: 50px; height: 50px;
@@ -176,6 +285,24 @@ div.header.small {
line-height: 40px; 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 { div.main {
padding: 6px 12px; padding: 6px 12px;
} }
@@ -189,7 +316,7 @@ div.buttonContainer>button {
} }
#loading { #loading {
position: absolute; position: fixed;
width: 64px; width: 64px;
height: 64px; height: 64px;
top: 50%; top: 50%;
@@ -200,12 +327,36 @@ div.buttonContainer>button {
visibility: hidden; 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) { @media screen and (max-width: 599px) {
div.header.small h1 span { div.header.small h1 span {
display: none; display: none;
} }
button { button, a.button {
padding: 0; padding: 0;
width: 100%; width: 100%;
} }
div.modal {
left: 20px;
right: 20px;
}
div.box {
width: initial;
}
}
@media screen and (min-width: 600px) {
div.modal {
width: 540px;
left: 50%;
margin-left: -270px;
}
} }
+15 -4
View File
@@ -4,11 +4,15 @@
div.mapItem { div.mapItem {
width: 350px; width: 350px;
background-color: #eeeeee;
border-radius: 3px;
margin: 10px auto; margin: 10px auto;
} }
div.mapItem.new {
display: flex;
justify-content: center;
align-items: center;
}
div.mapItem>div.title { div.mapItem>div.title {
background-color: #28a745; background-color: #28a745;
color: white; color: white;
@@ -27,7 +31,8 @@ div.mapItem>img {
} }
div.mapItem>div.inner { div.mapItem>div.inner {
padding: 8px; background-color: #eeeeee;
padding: 10px 8px;
} }
div.mapItem>div.inner>div.info { div.mapItem>div.inner>div.info {
@@ -43,6 +48,12 @@ div.mapItem>div.inner>div.info>p:nth-child(2) {
text-align: right; text-align: right;
} }
div.mapItem>div.buttonContainer {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
}
@media screen and (min-width: 1504px) { @media screen and (min-width: 1504px) {
#mapContainer { #mapContainer {
grid-template-columns: auto auto auto auto; grid-template-columns: auto auto auto auto;
@@ -69,6 +80,6 @@ div.mapItem>div.inner>div.info>p:nth-child(2) {
@media screen and (max-width: 374px) { @media screen and (max-width: 374px) {
div.mapItem { div.mapItem {
width: initial; width: 100%;
} }
} }
+103 -119
View File
@@ -1,7 +1,7 @@
'use strict'; 'use strict';
(function () { (function () {
var Core = { var Game = {
NUMBER_OF_ROUNDS: 5, NUMBER_OF_ROUNDS: 5,
MAX_SCORE: 1000, MAX_SCORE: 1000,
@@ -16,55 +16,50 @@
initialize: function () { initialize: function () {
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible'; document.getElementById('guessCover').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'; document.getElementById('currentScoreSum').innerHTML = '0/0';
Core.map.setOptions({ Game.map.setOptions({
draggableCursor: 'crosshair' draggableCursor: 'crosshair'
}); });
Core.map.fitBounds(mapBounds); Game.map.fitBounds(mapBounds);
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('guessCover').style.visibility = 'hidden';
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onload = function () {
if (this.response.error) { if (this.response.error) {
//TODO: handle this error //TODO: handle this error
return; return;
} }
document.getElementById('loading').style.visibility = 'hidden'; Game.panoId = this.response.panoId;
document.getElementById('cover').style.visibility = 'hidden';
Core.panoId = this.response.panoId;
if (this.response.history) { if (this.response.history) {
for (var i = 0; i < this.response.history.length; ++i) { for (var i = 0; i < this.response.history.length; ++i) {
var round = this.response.history[i]; var round = this.response.history[i];
Core.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null }); Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
Core.addRealGuessPair(round.position, round.guessPosition, true); Game.addRealGuessPair(round.position, round.guessPosition, true);
Core.scoreSum += round.score; Game.scoreSum += round.score;
} }
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);
document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_SCORE); 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.send();
}, },
resetGame: function () { reset: function () {
if (Core.guessMarker) { if (Game.guessMarker) {
Core.guessMarker.setMap(null); Game.guessMarker.setMap(null);
Core.guessMarker = null; Game.guessMarker = null;
} }
for (var i = 0; i < Core.rounds.length; ++i) { for (var i = 0; i < Game.rounds.length; ++i) {
var round = Core.rounds[i]; var round = Game.rounds[i];
if (round.realMarker && round.guessMarker && round.line) { if (round.realMarker && round.guessMarker && round.line) {
round.realMarker.setMap(null); round.realMarker.setMap(null);
@@ -73,8 +68,8 @@
} }
} }
Core.rounds = []; Game.rounds = [];
Core.scoreSum = 0; Game.scoreSum = 0;
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = null; distanceInfo.children[0].style.display = null;
@@ -89,142 +84,137 @@
document.getElementById('guess').style.visibility = null; document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result'); document.getElementById('guess').classList.remove('result');
Core.initialize(); Game.initialize();
}, },
resetRound: function () { resetRound: function () {
document.getElementById('scoreBar').style.width = null; document.getElementById('scoreBar').style.width = null;
if (Core.rounds.length > 0) { if (Game.rounds.length > 0) {
var lastRound = Core.rounds[Core.rounds.length - 1]; var lastRound = Game.rounds[Game.rounds.length - 1];
lastRound.realMarker.setVisible(false); lastRound.realMarker.setVisible(false);
lastRound.guessMarker.setVisible(false); lastRound.guessMarker.setVisible(false);
lastRound.line.setVisible(false); lastRound.line.setVisible(false);
} }
document.getElementById('cover').style.visibility = 'hidden'; document.getElementById('guessCover').style.visibility = 'hidden';
document.getElementById('showGuessButton').style.visibility = null; document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null; document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result') document.getElementById('guess').classList.remove('result')
Core.map.setOptions({ Game.map.setOptions({
draggableCursor: 'crosshair' draggableCursor: 'crosshair'
}); });
Core.map.fitBounds(mapBounds); Game.map.fitBounds(mapBounds);
Core.startNewRound(); Game.startNewRound();
}, },
startNewRound: function () { 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) { handleErrorResponse: function (error) {
// for the time being we only handle the "no_session_found" error and reset the game // for the time being we only handle the "no_session_found" error and reset the game
var xhr = new XMLHttpRequest(); MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
xhr.responseType = 'json';
xhr.onload = function () {
mapBounds = this.response.bounds; mapBounds = this.response.bounds;
Core.resetGame(); Game.reset();
}; });
xhr.open('GET', '/game/' + mapId + '/json', true);
xhr.send();
}, },
loadPano: function (panoId) { loadPano: function (panoId) {
if (Core.adaptGuess) { if (Game.adaptGuess) {
document.getElementById('guess').classList.add('adapt'); document.getElementById('guess').classList.add('adapt');
} }
Core.panorama.setPov({ heading: 0, pitch: 0 }); Game.panorama.setPov({ heading: 0, pitch: 0 });
Core.panorama.setZoom(0); Game.panorama.setZoom(0);
Core.panorama.setPano(panoId); Game.panorama.setPano(panoId);
}, },
evaluateGuess: function () { evaluateGuess: function () {
var guessPosition = Core.guessMarker.getPosition().toJSON(); if (!Game.guessMarker) {
Core.rounds[Core.rounds.length - 1].guessPosition = guessPosition; 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('guess').classList.remove('adapt');
} }
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible'; document.getElementById('guessCover').style.visibility = 'visible';
var data = new FormData(); var data = new FormData();
data.append('lat', String(guessPosition.lat)); data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng)); data.append('lng', String(guessPosition.lng));
var xhr = new XMLHttpRequest(); MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () {
xhr.responseType = 'json';
xhr.onload = function () {
if (this.response.error) { if (this.response.error) {
Core.handleErrorResponse(this.response.error); Game.handleErrorResponse(this.response.error);
return; return;
} }
Core.guessMarker.setMap(null); Game.guessMarker.setMap(null);
Core.guessMarker = null; Game.guessMarker = null;
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('guess').classList.add('result'); document.getElementById('guess').classList.add('result');
Core.scoreSum += this.response.result.score; Game.scoreSum += this.response.result.score;
document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_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; Game.rounds[Game.rounds.length - 1].position = this.response.result.position;
Core.addRealGuessPair(this.response.result.position, guessPosition); Game.addRealGuessPair(this.response.result.position, guessPosition);
var resultBounds = new google.maps.LatLngBounds(); var resultBounds = new google.maps.LatLngBounds();
resultBounds.extend(this.response.result.position); resultBounds.extend(this.response.result.position);
resultBounds.extend(guessPosition); resultBounds.extend(guessPosition);
Core.map.setOptions({ Game.map.setOptions({
draggableCursor: 'grab' draggableCursor: 'grab'
}); });
Core.map.fitBounds(resultBounds); Game.map.fitBounds(resultBounds);
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance); document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
document.getElementById('score').innerHTML = this.response.result.score; 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'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; scoreBar.style.width = scoreBarProperties.width;
if (Core.rounds.length === Core.NUMBER_OF_ROUNDS) { if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none'; document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block'; document.getElementById('showSummaryButton').style.display = 'block';
} }
Core.panoId = this.response.panoId; Game.panoId = this.response.panoId;
}; }, data);
xhr.open('POST', '/game/' + mapId + '/guess.json', true);
xhr.send(data);
}, },
addRealGuessPair: function (position, guessPosition, hidden) { 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({ round.realMarker = new google.maps.Marker({
map: Core.map, map: Game.map,
visible: !hidden, visible: !hidden,
position: position, position: position,
title: 'Open in Google Maps', title: 'Open in Google Maps',
zIndex: Core.rounds.length * 2, zIndex: Game.rounds.length * 2,
clickable: true, clickable: true,
draggable: false, draggable: false,
icon: { 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), size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32), scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32) anchor: new google.maps.Point(12, 32)
@@ -236,14 +226,14 @@
}); });
round.guessMarker = new google.maps.Marker({ round.guessMarker = new google.maps.Marker({
map: Core.map, map: Game.map,
visible: !hidden, visible: !hidden,
position: guessPosition, position: guessPosition,
zIndex: Core.rounds.length, zIndex: Game.rounds.length,
clickable: false, clickable: false,
draggable: false, draggable: false,
icon: { 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), size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32), scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32), anchor: new google.maps.Point(12, 32),
@@ -259,7 +249,7 @@
}); });
round.line = new google.maps.Polyline({ round.line = new google.maps.Polyline({
map: Core.map, map: Game.map,
visible: !hidden, visible: !hidden,
path: [ path: [
position, position,
@@ -310,11 +300,11 @@
var resultBounds = new google.maps.LatLngBounds(); var resultBounds = new google.maps.LatLngBounds();
for (var i = 0; i < Core.rounds.length; ++i) { for (var i = 0; i < Game.rounds.length; ++i) {
var round = Core.rounds[i]; var round = Game.rounds[i];
round.realMarker.setIcon({ 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), size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32), scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32), anchor: new google.maps.Point(12, 32),
@@ -335,32 +325,32 @@
resultBounds.extend(round.guessPosition); 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'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; scoreBar.style.width = scoreBarProperties.width;
}, },
rewriteGoogleLink: function () { rewriteGoogleLink: function () {
if (!Core.googleLink) { if (!Game.googleLink) {
var anchors = document.getElementById('panorama').getElementsByTagName('a'); var anchors = document.getElementById('panorama').getElementsByTagName('a');
for (var i = 0; i < anchors.length; i++) { for (var i = 0; i < anchors.length; i++) {
var a = anchors[i]; var a = anchors[i];
if (a.href.indexOf('maps.google.com/maps') !== -1) { if (a.href.indexOf('maps.google.com/maps') !== -1) {
Core.googleLink = a; Game.googleLink = a;
break; break;
} }
} }
} }
setTimeout(function () { setTimeout(function () {
if (Core.googleLink) { if (Game.googleLink) {
Core.googleLink.title = 'Google Maps'; Game.googleLink.title = 'Google Maps';
Core.googleLink.href = 'https://maps.google.com/maps'; Game.googleLink.href = 'https://maps.google.com/maps';
} }
}, 1); }, 1);
} }
@@ -381,32 +371,32 @@
}; };
if (!('ontouchstart' in document.documentElement)) { 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, disableDefaultUI: true,
clickableIcons: false, clickableIcons: false,
draggingCursor: 'grabbing' draggingCursor: 'grabbing'
}); });
Core.map.addListener('click', function (e) { Game.map.addListener('click', function (e) {
if (Core.rounds[Core.rounds.length - 1].guessPosition) { if (Game.rounds[Game.rounds.length - 1].guessPosition) {
return; return;
} }
if (Core.guessMarker) { if (Game.guessMarker) {
Core.guessMarker.setPosition(e.latLng); Game.guessMarker.setPosition(e.latLng);
return; return;
} }
Core.guessMarker = new google.maps.Marker({ Game.guessMarker = new google.maps.Marker({
map: Core.map, map: Game.map,
position: e.latLng, position: e.latLng,
clickable: false, clickable: false,
draggable: true, draggable: true,
icon: { 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), size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32), scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32), anchor: new google.maps.Point(12, 32),
@@ -424,22 +414,22 @@
document.getElementById('guessButton').disabled = false; 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, disableDefaultUI: true,
linksControl: true, linksControl: true,
showRoadLabels: false, showRoadLabels: false,
motionTracking: false motionTracking: false
}); });
Core.panorama.addListener('position_changed', function () { Game.panorama.addListener('position_changed', function () {
Core.rewriteGoogleLink(); Game.rewriteGoogleLink();
}); });
Core.panorama.addListener('pov_changed', function () { Game.panorama.addListener('pov_changed', function () {
Core.rewriteGoogleLink(); Game.rewriteGoogleLink();
}); });
Core.initialize(); Game.initialize();
document.getElementById('showGuessButton').onclick = function () { document.getElementById('showGuessButton').onclick = function () {
this.style.visibility = 'hidden'; this.style.visibility = 'hidden';
@@ -452,24 +442,18 @@
} }
document.getElementById('guessButton').onclick = function () { document.getElementById('guessButton').onclick = function () {
if (!Core.guessMarker) { Game.evaluateGuess();
return;
}
this.disabled = true;
Core.evaluateGuess();
} }
document.getElementById('continueButton').onclick = function () { document.getElementById('continueButton').onclick = function () {
Core.resetRound(); Game.resetRound();
} }
document.getElementById('showSummaryButton').onclick = function () { document.getElementById('showSummaryButton').onclick = function () {
Core.showSummary(); Game.showSummary();
} }
document.getElementById('startNewGameButton').onclick = function () { document.getElementById('startNewGameButton').onclick = function () {
Core.resetGame(); Game.reset();
} }
})(); })();
+37
View File
@@ -0,0 +1,37 @@
(function () {
var form = document.getElementById('loginForm');
form.onsubmit = function (e) {
document.getElementById('loading').style.visibility = 'visible';
e.preventDefault();
var formData = new FormData(form);
MapGuesser.httpRequest('POST', form.action, 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('/');
}, formData);
};
})();
+331 -43
View File
@@ -1,39 +1,113 @@
'use strict';
(function () { (function () {
var MapEditor = { var MapEditor = {
metadata: {
name: null,
description: null
},
map: null, map: null,
markers: null,
panorama: null, panorama: null,
selectedMarker: null, selectedMarker: null,
added: {},
edited: {},
deleted: {},
editMetadata: function () {
var form = document.getElementById('metadataForm');
MapEditor.metadata.name = form.elements.name.value;
MapEditor.metadata.description = form.elements.description.value;
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
MapGuesser.hideModal();
document.getElementById('saveButton').disabled = false;
},
getPlace: function (placeId, marker) { getPlace: function (placeId, marker) {
var xhr = new XMLHttpRequest(); MapGuesser.httpRequest('GET', '/admin/place.json/' + placeId, function () {
xhr.responseType = 'json';
xhr.onload = function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
if (!this.response.panoId) { if (!this.response.panoId) {
document.getElementById('noPano').style.visibility = 'visible'; document.getElementById('noPano').style.visibility = 'visible';
marker.noPano = true; places[marker.placeId].panoId = -1;
places[marker.placeId].noPano = true;
return; return;
} }
MapEditor.loadPano(this.response.panoId); places[marker.placeId].panoId = this.response.panoId;
}; places[marker.placeId].noPano = false;
xhr.open('GET', '/admin/place.json/' + placeId, true); MapEditor.loadPano(this.response.panoId, places[marker.placeId].pov);
xhr.send(); });
}, },
loadPano: function (panoId) { loadPano: function (panoId, pov) {
MapEditor.panorama.setVisible(true); MapEditor.panorama.setVisible(true);
MapEditor.panorama.setPov({ heading: 0, pitch: 0 }); MapEditor.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
MapEditor.panorama.setZoom(0); MapEditor.panorama.setZoom(pov.zoom);
MapEditor.panorama.setPano(panoId); MapEditor.panorama.setPano(panoId);
}, },
loadPanoForNewPlace: function (panoLocationData) {
var placeId = MapEditor.selectedMarker.placeId;
if (!panoLocationData) {
places[placeId].panoId = -1;
places[placeId].noPano = true;
document.getElementById('noPano').style.visibility = 'visible';
return;
}
var latLng = panoLocationData.latLng;
places[placeId].panoId = panoLocationData.pano;
places[placeId].lat = latLng.lat();
places[placeId].lng = latLng.lng();
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
MapEditor.map.panTo(MapEditor.selectedMarker.getLatLng());
MapEditor.panorama.setVisible(true);
MapEditor.panorama.setPov({ heading: 0.0, pitch: 0.0 });
MapEditor.panorama.setZoom(0.0);
MapEditor.panorama.setPano(panoLocationData.pano);
},
requestPanoData: function (location, canBeIndoor) {
var sv = new google.maps.StreetViewService();
sv.getPanorama({
location: location,
preference: google.maps.StreetViewPreference.NEAREST,
radius: 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) { select: function (marker) {
document.getElementById('loading').style.visibility = 'visible'; if (MapEditor.selectedMarker === marker) {
MapEditor.closePlace();
return;
}
document.getElementById('map').classList.add('selected'); document.getElementById('map').classList.add('selected');
document.getElementById('control').classList.add('selected'); document.getElementById('control').classList.add('selected');
@@ -44,40 +118,214 @@
MapEditor.resetSelected(); MapEditor.resetSelected();
MapEditor.selectedMarker = marker; MapEditor.selectedMarker = marker;
marker.setIcon(IconCollection.iconBlue);
marker.setZIndexOffset(2000);
MapEditor.map.invalidateSize(true); MapEditor.map.invalidateSize(true);
MapEditor.map.panTo(marker.getLatLng()); MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false); MapEditor.panorama.setVisible(false);
MapEditor.getPlace(marker.placeId, marker); 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) { if (!MapEditor.selectedMarker) {
return; return;
} }
MapEditor.selectedMarker.setIcon(MapEditor.selectedMarker.noPano ? IconCollection.iconRed : IconCollection.iconGreen); var placeId = MapEditor.selectedMarker.placeId
MapEditor.selectedMarker.setZIndexOffset(1000);
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('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]));
}
MapGuesser.httpRequest('POST', '/admin/saveMap/' + mapId + '/json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
//TODO: handle this error
return;
}
MapEditor.replacePlaceIdsToReal(this.response.added);
if (mapId === 0) {
mapId = this.response.mapId;
window.history.replaceState(null, '', '/admin/mapEditor/' + mapId);
}
MapEditor.added = {};
MapEditor.edited = {};
MapEditor.deleted = {};
document.getElementById('added').innerHTML = '0';
document.getElementById('edited').innerHTML = '0';
document.getElementById('deleted').innerHTML = '0';
document.getElementById('saveButton').disabled = true;
}, data);
},
replacePlaceIdsToReal: function (addedPlaces) {
for (var i = 0; i < addedPlaces.length; ++i) {
var tempId = addedPlaces[i].tempId;
var placeId = addedPlaces[i].id;
places[tempId].id = placeId;
}
} }
}; };
var IconCollection = { var IconCollection = {
iconGreen: L.icon({ iconGreen: L.icon({
iconUrl: '/static/img/markers/marker-green.svg', iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32], iconSize: [24, 32],
iconAnchor: [12, 32] iconAnchor: [12, 32]
}), }),
iconRed: L.icon({ iconRed: L.icon({
iconUrl: '/static/img/markers/marker-red.svg', iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32], iconSize: [24, 32],
iconAnchor: [12, 32] iconAnchor: [12, 32]
}), }),
iconBlue: L.icon({ iconBlue: L.icon({
iconUrl: '/static/img/markers/marker-blue.svg', iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32], iconSize: [24, 32],
iconAnchor: [12, 32] iconAnchor: [12, 32]
}), }),
@@ -96,41 +344,61 @@
}; };
MapEditor.map = L.map('map', { MapEditor.map = L.map('map', {
attributionControl: false,
zoomControl: false zoomControl: false
}); });
MapEditor.map.on('click', function (e) {
var marker = L.marker(e.latlng, {
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
});
var highResData = Util.getHighResData(); var highResData = Util.getHighResData();
L.tileLayer(tileUrl, { L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: '1234', subdomains: '1234',
ppi: highResData.ppi, ppi: highResData.ppi,
tileSize: highResData.tileSize, tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset, zoomOffset: highResData.zoomOffset,
minZoom: 0, minZoom: 2,
maxZoom: 20 maxZoom: 20
}).addTo(MapEditor.map); }).addTo(MapEditor.map);
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east })); 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) { MapEditor.markers = L.markerClusterGroup({
var marker = L.marker({ lat: places[i].lat, lng: places[i].lng }, { maxClusterRadius: 50
icon: places[i].noPano ? IconCollection.iconRed : IconCollection.iconGreen, });
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 zIndexOffset: 1000
}) })
.addTo(MapEditor.map) .addTo(MapEditor.markers)
.on('click', function () { .on('click', function () {
if (MapEditor.selectedMarker === this) {
return;
}
MapEditor.select(this); MapEditor.select(this);
}); });
marker.placeId = places[i].id; marker.placeId = place.id;
marker.noPano = places[i].noPano;
} }
MapEditor.map.addLayer(MapEditor.markers);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), { MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work // switch off fullscreenControl because positioning doesn't work
fullscreenControl: false, fullscreenControl: false,
@@ -140,16 +408,36 @@
motionTracking: false motionTracking: false
}); });
document.getElementById('cancelButton').onclick = function () { document.getElementById('mapName').onclick = function (e) {
document.getElementById('map').classList.remove('selected'); e.preventDefault();
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(); MapGuesser.showModal('metadata');
MapEditor.selectedMarker = null; document.getElementById('metadataForm').elements.name.select();
};
MapEditor.map.invalidateSize(true); document.getElementById('metadataForm').onsubmit = function (e) {
e.preventDefault();
MapEditor.editMetadata();
};
document.getElementById('closeMetadataButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('saveButton').onclick = function () {
MapEditor.saveMap();
};
document.getElementById('applyButton').onclick = function () {
MapEditor.applyPlace();
};
document.getElementById('closeButton').onclick = function () {
MapEditor.closePlace();
};
document.getElementById('deleteButton').onclick = function () {
MapEditor.deletePlace();
}; };
})(); })();
+107
View File
@@ -0,0 +1,107 @@
var MapGuesser = {
httpRequest: function (method, url, callback, data) {
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onload = callback;
xhr.open(method, url, true);
if (method === 'POST') {
if (typeof data === 'undefined') {
data = new FormData();
}
data.append('anti_csrf_token', ANTI_CSRF_TOKEN);
xhr.send(data);
} else {
xhr.send();
}
},
showModal: function (id) {
document.getElementById(id).style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible';
},
showModalWithContent: function (title, body, extraButtons) {
if (typeof extraButtons === 'undefined') {
extraButtons = [];
}
MapGuesser.showModal('modal');
document.getElementById('modalTitle').textContent = title;
if (typeof body === 'object') {
document.getElementById('modalText').appendChild(body);
} else {
document.getElementById('modalText').textContent = body;
}
var buttons = document.getElementById('modalButtons');
buttons.textContent = '';
for (extraButton of extraButtons) {
var button = document.createElement(extraButton.type);
if (extraButton.type === 'a') {
button.classList.add('button');
}
for (className of extraButton.classNames) {
button.classList.add(className);
}
button.classList.add('marginTop');
button.classList.add('marginRight');
button.textContent = extraButton.text;
if (extraButton.type === 'a') {
button.href = extraButton.href;
} else if (extraButton.type === 'button') {
button.onclick = extraButton.onclick;
}
buttons.appendChild(button);
}
var closeButton = document.createElement('button');
closeButton.classList.add('gray');
closeButton.classList.add('marginTop');
closeButton.textContent = 'Cancel';
closeButton.onclick = function () {
MapGuesser.hideModal();
};
buttons.appendChild(closeButton);
},
hideModal: function () {
var modals = document.getElementsByClassName('modal');
for (modal of modals) {
modal.style.visibility = 'hidden';
}
document.getElementById('cover').style.visibility = 'hidden';
}
};
(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';
}
}
}
document.getElementById('cover').onclick = function () {
MapGuesser.hideModal();
};
})();
+2 -1
View File
@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"leaflet": "^1.6.0" "leaflet": "^1.6.0",
"leaflet.markercluster": "^1.4.1"
} }
} }
+5
View File
@@ -2,6 +2,11 @@
# yarn lockfile v1 # 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: leaflet@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
+1 -6
View File
@@ -74,8 +74,7 @@ def updateAppInWorktree(worktreePath):
subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath) subprocess.call([worktreePath + "/scripts/update.sh"], cwd=worktreePath)
def updateAppVersionInWorktree(worktreePath): def updateAppVersionInWorktree(worktreePath):
pass subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
#subprocess.call([worktreePath + "/scripts/update-version.sh"], cwd=worktreePath)
worktrees = getDataForWorktrees() worktrees = getDataForWorktrees()
@@ -111,10 +110,6 @@ if developmentWorktree.revision != developmentWorktree.newRevision:
elif developmentWorktree.version != developmentWorktree.newVersion: elif developmentWorktree.version != developmentWorktree.newVersion:
print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED") print("-> DEVELOPMENT " + developmentWorktree.path + "'s version info will be UPDATED")
checkoutWorktree(developmentWorktree.path, developmentWorktree.branch)
print(developmentWorktree.path + " is checked out to " + developmentWorktree.branch)
updateAppVersionInWorktree(developmentWorktree.path) updateAppVersionInWorktree(developmentWorktree.path)
print("MapGuesser version is updated in " + developmentWorktree.path) print("MapGuesser version is updated in " + developmentWorktree.path)
+15
View File
@@ -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
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
cd ${ROOT_DIR}
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
+51
View File
@@ -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;
}
}
+28 -27
View File
@@ -1,53 +1,54 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Repository\MapRepository;
class GameController 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); $data = $this->prepareGame($mapId);
return new HtmlContent('game', $data); 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); $data = $this->prepareGame($mapId);
return new JsonContent($data); return new JsonContent($data);
} }
private function prepareGame(int $mapId) private function prepareGame(int $mapId)
{ {
$bounds = $this->getMapBounds($mapId); $map = $this->mapRepository->getById($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);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']); $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' => $map['area'],
'rounds' => []
]);
}
return ['mapId' => $mapId, 'mapName' => $map['name'], 'bounds' => $bounds->toArray()];
} }
} }
@@ -1,43 +1,50 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
class PositionController class GameFlowController
{ {
const NUMBER_OF_ROUNDS = 5; const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000; const MAX_SCORE = 1000;
private IRequest $request;
private PlaceRepository $placeRepository; private PlaceRepository $placeRepository;
public function __construct() public function __construct(IRequest $request)
{ {
$this->request = $request;
$this->placeRepository = new PlaceRepository(); $this->placeRepository = new PlaceRepository();
} }
public function getPosition(array $parameters): IContent public function getNewPlace(): IContent
{ {
$mapId = (int) $parameters['mapId']; $mapId = (int) $this->request->query('mapId');
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) { $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found']; $data = ['error' => 'no_session_found'];
return new JsonContent($data); return new JsonContent($data);
} }
if (count($_SESSION['state']['rounds']) === 0) { if (count($state['rounds']) === 0) {
$newPosition = $this->placeRepository->getForMapWithValidPano($mapId); $place = $this->placeRepository->getForMapWithValidPano($mapId);
$_SESSION['state']['rounds'][] = $newPosition; $state['rounds'][] = $place;
$session->set('state', $state);
$data = ['panoId' => $newPosition['panoId']]; $data = ['panoId' => $place['panoId']];
} else { } else {
$rounds = count($_SESSION['state']['rounds']); $rounds = count($state['rounds']);
$last = $_SESSION['state']['rounds'][$rounds - 1]; $last = $state['rounds'][$rounds - 1];
$history = []; $history = [];
for ($i = 0; $i < $rounds - 1; ++$i) { for ($i = 0; $i < $rounds - 1; ++$i) {
$round = $_SESSION['state']['rounds'][$i]; $round = $state['rounds'][$i];
$history[] = [ $history[] = [
'position' => $round['position']->toArray(), 'position' => $round['position']->toArray(),
'guessPosition' => $round['guessPosition']->toArray(), 'guessPosition' => $round['guessPosition']->toArray(),
@@ -55,41 +62,45 @@ class PositionController
return new JsonContent($data); return new JsonContent($data);
} }
public function evaluateGuess(array $parameters): IContent public function evaluateGuess(): IContent
{ {
$mapId = (int) $parameters['mapId']; $mapId = (int) $this->request->query('mapId');
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) { $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$data = ['error' => 'no_session_found']; $data = ['error' => 'no_session_found'];
return new JsonContent($data); return new JsonContent($data);
} }
$last = &$_SESSION['state']['rounds'][count($_SESSION['state']['rounds']) - 1]; $last = $state['rounds'][count($state['rounds']) - 1];
$position = $last['position']; $position = $last['position'];
$guessPosition = new Position((float) $_POST['lat'], (float) $_POST['lng']); $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$last['guessPosition'] = $guessPosition;
$distance = $this->calculateDistance($position, $guessPosition); $distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $_SESSION['state']['area']); $score = $this->calculateScore($distance, $state['area']);
$last['guessPosition'] = $guessPosition;
$last['distance'] = $distance; $last['distance'] = $distance;
$last['score'] = $score; $last['score'] = $score;
$state['rounds'][count($state['rounds']) - 1] = $last;
if (count($_SESSION['state']['rounds']) < static::NUMBER_OF_ROUNDS) { if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
$exclude = []; $exclude = [];
foreach ($_SESSION['state']['rounds'] as $round) { foreach ($state['rounds'] as $round) {
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]); $exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
} }
$newPosition = $this->placeRepository->getForMapWithValidPano($mapId, $exclude); $place = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
$_SESSION['state']['rounds'][] = $newPosition; $state['rounds'][] = $place;
$session->set('state', $state);
$panoId = $newPosition['panoId']; $panoId = $place['panoId'];
} else { } else {
$_SESSION['state']['rounds'] = []; $state['rounds'] = [];
$session->set('state', $state);
$panoId = null; $panoId = null;
} }
@@ -112,7 +123,7 @@ class PositionController
private function calculateScore(float $distance, float $area): int private function calculateScore(float $distance, float $area): int
{ {
$goodness = 1.0 - ($distance / sqrt($area)); $goodness = 1.0 - ($distance / (sqrt($area) * 1000));
return (int) round(pow(static::MAX_SCORE, $goodness)); return (int) round(pow(static::MAX_SCORE, $goodness));
} }
+73
View File
@@ -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);
}
}
+181 -21
View File
@@ -1,44 +1,68 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
class MapAdminController class MapAdminController implements ISecured
{ {
private static string $unnamedMapName = '[unnamed map]';
private IRequest $request;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository; private PlaceRepository $placeRepository;
public function __construct() public function __construct(IRequest $request)
{ {
$this->request = $request;
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository(); $this->placeRepository = new PlaceRepository();
} }
public function getMaps(): IContent public function authorize(): bool
{ {
//TODO $user = $this->request->user();
return new HtmlContent('admin/maps'); return $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
} }
public function getMapEditor(array $parameters): IContent public function getMapEditor(): IContent
{ {
$mapId = (int) $parameters['mapId']; $mapId = (int) $this->request->query('mapId');
$bounds = $this->getMapBounds($mapId); 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 = [];
}
$places = $this->getPlaces($mapId); $data = ['mapId' => $mapId, 'mapName' => $map['name'], 'mapDescription' => str_replace('<br>', "\n", $map['description']), 'bounds' => $bounds->toArray(), 'places' => &$places];
$data = ['mapId' => $mapId, 'bounds' => $bounds->toArray(), 'places' => &$places];
return new HtmlContent('admin/map_editor', $data); return new HtmlContent('admin/map_editor', $data);
} }
public function getPlace(array $parameters) public function getPlace(): IContent
{ {
$placeId = (int) $parameters['placeId']; $placeId = (int) $this->request->query('placeId');
$placeData = $this->placeRepository->getById($placeId); $placeData = $this->placeRepository->getById($placeId);
@@ -46,34 +70,170 @@ class MapAdminController
return new JsonContent($data); return new JsonContent($data);
} }
private function getMapBounds(int $mapId): Bounds public function saveMap(): IContent
{ {
$select = new Select(\Container::$dbConnection, 'maps'); $mapId = (int) $this->request->query('mapId');
$select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
$select->whereId($mapId);
$map = $select->execute()->fetch(IResultSet::FETCH_ASSOC); \Container::$dbConnection->startTransaction();
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']); 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(),
'area' => $mapBounds->calculateApproximateArea(),
];
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);
\Container::$dbConnection->commit();
$data = ['mapId' => $mapId, 'added' => $addedIds];
return new JsonContent($data);
}
public function deleteMap() {
$mapId = (int) $this->request->query('mapId');
\Container::$dbConnection->startTransaction();
$this->deletePlaces($mapId);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->delete();
\Container::$dbConnection->commit();
$data = ['success' => true];
return new JsonContent($data);
}
private function deletePlaces(int $mapId): void
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id']);
$select->where('map_id', '=', $mapId);
$result = $select->execute();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($place['id']);
$modify->delete();
}
}
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; 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 private function &getPlaces(int $mapId): array
{ {
$select = new Select(\Container::$dbConnection, 'places'); $select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']); $select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $mapId); $select->where('map_id', '=', $mapId);
$select->orderBy('lng');
$result = $select->execute(); $result = $select->execute();
$places = []; $places = [];
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) { while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
//$panoId = ???
//$pov = ???
$noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null; $noPano = $place['pano_id_cached_timestamp'] && $place['pano_id_cached'] === null;
$places[] = ['id' => $place['id'], 'lat' => $place['lat'], 'lng' => $place['lng'], 'noPano' => $noPano]; $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; return $places;
+30 -13
View File
@@ -2,13 +2,21 @@
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Database\RawExpression; use MapGuesser\Database\RawExpression;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
class MapsController class MapsController
{ {
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getMaps(): IContent public function getMaps(): IContent
{ {
$select = new Select(\Container::$dbConnection, 'maps'); $select = new Select(\Container::$dbConnection, 'maps');
@@ -20,43 +28,52 @@ class MapsController
['maps', 'bound_west_lng'], ['maps', 'bound_west_lng'],
['maps', 'bound_north_lat'], ['maps', 'bound_north_lat'],
['maps', 'bound_east_lng'], ['maps', 'bound_east_lng'],
['maps', 'area'],
new RawExpression('COUNT(places.id) AS num_places') new RawExpression('COUNT(places.id) AS num_places')
]); ]);
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']); $select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
$select->groupBy(['maps', 'id']);
$select->orderBy('name'); $select->orderBy('name');
$result = $select->execute(); $result = $select->execute();
$maps = []; $maps = [];
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) { while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']); $map['area'] = $this->formatMapAreaForHuman($map['area']);
$map['area'] = $this->formatMapAreaForHuman($bounds->calculateApproximateArea());
$maps[] = $map; $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); return new HtmlContent('maps', $data);
} }
private function formatMapAreaForHuman(float $area): array private function formatMapAreaForHuman(float $area): array
{ {
if ($area < 100000.0) { if ($area < 0.01) {
$digits = 0;
$rounded = round($area * 1000000.0, -2);
$unit = 'm';
} elseif ($area < 0.1) {
$digits = 0;
$rounded = round($area * 1000000.0, -3);
$unit = 'm';
} elseif ($area < 1.0) {
$digits = 2;
$rounded = round($area, 2);
$unit = 'km';
} elseif ($area < 100.0) {
$digits = 0; $digits = 0;
$rounded = round($area, 0); $rounded = round($area, 0);
$unit = 'm';
} elseif ($area < 100000000.0) {
$digits = 0;
$rounded = round($area / 1000000.0, 0);
$unit = 'km'; $unit = 'km';
} elseif ($area < 10000000000.0) { } elseif ($area < 10000.0) {
$digits = 0; $digits = 0;
$rounded = round($area / 1000000.0, -2); $rounded = round($area, -2);
$unit = 'km'; $unit = 'km';
} else { } else {
$digits = 0; $digits = 0;
$rounded = round($area / 1000000.0, -4); $rounded = round($area, -4);
$unit = 'km'; $unit = 'km';
} }
+22 -3
View File
@@ -16,6 +16,8 @@ class Modify
private array $original = []; private array $original = [];
private ?string $externalId = null;
private bool $autoIncrement = true; private bool $autoIncrement = true;
public function __construct(IConnection $connection, string $table) public function __construct(IConnection $connection, string $table)
@@ -31,6 +33,13 @@ class Modify
return $this; return $this;
} }
public function setExternalId($id): Modify
{
$this->externalId = $id;
return $this;
}
public function setAutoIncrement(bool $autoIncrement = true): Modify public function setAutoIncrement(bool $autoIncrement = true): Modify
{ {
$this->autoIncrement = $autoIncrement; $this->autoIncrement = $autoIncrement;
@@ -59,6 +68,11 @@ class Modify
return $this; return $this;
} }
public function getId()
{
return $this->attributes[$this->idName];
}
public function save(): void public function save(): void
{ {
if (isset($this->attributes[$this->idName])) { if (isset($this->attributes[$this->idName])) {
@@ -82,7 +96,9 @@ class Modify
private function insert(): void private function insert(): void
{ {
if (!$this->autoIncrement) { if ($this->externalId !== null) {
$this->attributes[$this->idName] = $this->externalId;
} elseif (!$this->autoIncrement) {
$this->attributes[$this->idName] = $this->generateKey(); $this->attributes[$this->idName] = $this->generateKey();
} }
@@ -100,11 +116,14 @@ class Modify
private function update(): void private function update(): void
{ {
$diff = $this->generateDiff(); /*$diff = $this->generateDiff();
if (count($diff) === 0) { if (count($diff) === 0) {
return; return;
} }*/
$diff = $this->attributes;
unset($diff[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($diff)); $set = $this->generateColumnsWithBinding(array_keys($diff));
+10
View File
@@ -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;
}
+14
View File
@@ -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;
}
+12
View File
@@ -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;
}
+49
View File
@@ -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;
}
}
+70
View File
@@ -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);
}
}
+16
View File
@@ -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', 'area']);
$select->whereId($mapId);
return $select->execute()->fetch(IResultSet::FETCH_ASSOC);
}
}
+29 -2
View File
@@ -34,7 +34,7 @@ class PlaceRepository
$panoId = $this->requestPanoId($place); $panoId = $this->requestPanoId($place);
if ($panoId === null) { if ($panoId === null) {
$placesWithoutPano[] = $place['id']; $placesWithoutPano[] = $exclude[] = $place['id'];
} }
} while ($panoId === null); } while ($panoId === null);
@@ -48,6 +48,33 @@ class PlaceRepository
]; ];
} }
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 private function selectFromDbById(int $placeId): array
{ {
$select = new Select(\Container::$dbConnection, 'places'); $select = new Select(\Container::$dbConnection, 'places');
@@ -66,7 +93,7 @@ class PlaceRepository
$select->where('id', 'NOT IN', $exclude); $select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId); $select->where('map_id', '=', $mapId);
$numberOfPlaces = $select->count();// TODO: what if 0 $numberOfPlaces = $select->count(); // TODO: what if 0
$randomOffset = random_int(0, $numberOfPlaces - 1); $randomOffset = random_int(0, $numberOfPlaces - 1);
$select->orderBy('id'); $select->orderBy('id');
+57
View File
@@ -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');
}
}
+37
View File
@@ -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]);
}
}
+5 -8
View File
@@ -20,6 +20,11 @@ class Route
return $this->id; return $this->id;
} }
public function getHandler(): array
{
return $this->handler;
}
public function generateLink(array $parameters = []): string public function generateLink(array $parameters = []): string
{ {
$link = []; $link = [];
@@ -51,14 +56,6 @@ class Route
return implode('/', $link) . $query; 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 public function testAgainst(array $path): ?array
{ {
$parameters = []; $parameters = [];
+113
View File
@@ -0,0 +1,113 @@
<?php namespace MapGuesser\Session;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use SessionHandlerInterface;
use SessionIdInterface;
use SessionUpdateTimestampHandlerInterface;
class DatabaseSessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
{
private bool $exists = false;
private bool $written = false;
public function open($savePath, $sessionName): bool
{
return true;
}
public function close(): bool
{
return true;
}
public function read($id): string
{
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['data']);
$select->whereId($id);
$result = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($result === null) {
return '';
}
$this->exists = true;
return $result['data'];
}
public function write($id, $data): bool
{
$modify = new Modify(\Container::$dbConnection, 'sessions');
if ($this->exists) {
$modify->setId($id);
} else {
$modify->setExternalId($id);
}
$modify->set('data', $data);
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
$written = true;
return true;
}
public function destroy($id): bool
{
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id);
$modify->delete();
return true;
}
public function gc($maxlifetime): int
{
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-' . $maxlifetime . ' seconds'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
return true;
}
public function create_sid(): string
{
return hash('sha256', random_bytes(10) . microtime());
}
public function validateId($id): bool
{
return preg_match('/^[a-f0-9]{64}$/', $id);
}
public function updateTimestamp($id, $data): bool
{
if ($this->written) {
return true;
}
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($id);
$modify->set('updated', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
return true;
}
}
+24 -4
View File
@@ -2,7 +2,7 @@
class Bounds class Bounds
{ {
const ONE_DEGREE_OF_LATITUDE_IN_METER = 111132.954; const ONE_DEGREE_OF_LATITUDE_IN_KM = 111.132954;
private float $southLat = 90.0; private float $southLat = 90.0;
private float $westLng = 180.0; private float $westLng = 180.0;
@@ -59,14 +59,34 @@ 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 public function calculateApproximateArea(): float
{ {
$dLat = $this->northLat - $this->southLat; $dLat = $this->northLat - $this->southLat;
$dLng = $this->eastLng - $this->westLng; $dLng = $this->eastLng - $this->westLng;
$m = $dLat * static::ONE_DEGREE_OF_LATITUDE_IN_METER; $m = $dLat * static::ONE_DEGREE_OF_LATITUDE_IN_KM;
$a = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_METER * cos(deg2rad($this->northLat)); $a = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_KM * cos(deg2rad($this->northLat));
$c = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_METER * cos(deg2rad($this->southLat)); $c = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_KM * cos(deg2rad($this->southLat));
return $m * ($a + $c) / 2; return $m * ($a + $c) / 2;
} }
+67 -10
View File
@@ -1,10 +1,69 @@
<?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/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" title="Edit map data"><?= $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" class="modal">
<h2>Edit map data</h2>
<form id="metadataForm" class="marginTop">
<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 class="right">
<button class="marginTop marginRight" type="submit">Apply</button><!--
--><button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
</div>
</form>
</div>
<div id="map"></div> <div id="map"></div>
<div id="control"> <div id="control">
<button id="saveButton" class="fullWidth">Save</button> <button id="saveButton" class="fullWidth" disabled>Save</button>
<a class="button gray fullWidth marginTop" href="/admin/maps" title="Back to maps">Back to maps</a>
</div> </div>
<div id="panorama"></div> <div id="panorama"></div>
<div id="noPano"> <div id="noPano">
@@ -12,16 +71,14 @@
</div> </div>
<div id="placeControl"> <div id="placeControl">
<button id="applyButton" class="fullWidth">Apply</button> <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> <button id="deleteButton" class="red fullWidth marginTop">Delete</button>
</div> </div>
<script> <script>
var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>'; var tileUrl = '<?= $_ENV['LEAFLET_TILESERVER_URL'] ?>';
var mapId = '<?= $mapId ?>'; var tileAttribution = '<?= $_ENV['LEAFLET_TILESERVER_ATTRIBUTION'] ?>';
var mapId = <?= $mapId ?>;
var mapBounds = <?= json_encode($bounds) ?>; var mapBounds = <?= json_encode($bounds) ?>;
var places = <?= json_encode($places) ?>; var places = <?= json_encode($places, JSON_FORCE_OBJECT) ?>;
</script> </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'; ?> <?php require ROOT . '/views/templates/main_footer.php'; ?>
-7
View File
@@ -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'; ?>
+17 -7
View File
@@ -1,17 +1,29 @@
<?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'; ?> <?php require ROOT . '/views/templates/main_header.php'; ?>
<div class="header small"> <div class="header small">
<div class="grid"> <div class="grid">
<h1> <h1>
<a href="/maps" title="Back to playable maps"> <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> <span>MapGuesser</span>
</a> </a>
</h1> </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> </div>
<div id="cover"></div> <div id="guessCover"></div>
<div id="panorama"></div> <div id="panorama"></div>
<div id="showGuessButtonContainer"> <div id="showGuessButtonContainer">
<button id="showGuessButton" class="fullWidth">Show guess map</button> <button id="showGuessButton" class="fullWidth">Show guess map</button>
@@ -46,9 +58,7 @@
</div> </div>
</div> </div>
<script> <script>
var mapId = '<?= $mapId ?>'; var mapId = <?= $mapId ?>;
var mapBounds = <?= json_encode($bounds) ?>; var mapBounds = <?= json_encode($bounds) ?>;
</script> </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'; ?> <?php require ROOT . '/views/templates/main_footer.php'; ?>
+21
View File
@@ -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'; ?>
+62 -5
View File
@@ -1,10 +1,14 @@
<?php $cssFiles = ['/static/css/maps.css']; ?> <?php
$cssFiles = [
'css/maps.css'
];
?>
<?php require ROOT . '/views/templates/main_header.php'; ?> <?php require ROOT . '/views/templates/main_header.php'; ?>
<?php require ROOT . '/views/templates/header.php'; ?> <?php require ROOT . '/views/templates/header.php'; ?>
<div class="main"> <div class="main">
<h2>Playable maps</h2> <h2>Playable maps</h2>
<div id="mapContainer"> <div id="mapContainer">
<?php foreach ($maps as $map) : ?> <?php foreach ($maps as $map): ?>
<div class="mapItem"> <div class="mapItem">
<div class="title"> <div class="title">
<p class="title"><?= $map['name'] ?></p> <p class="title"><?= $map['name'] ?></p>
@@ -30,14 +34,67 @@
</div> </div>
<p class="small justify marginTop"><?= $map['description'] ?></p> <p class="small justify marginTop"><?= $map['description'] ?></p>
</div> </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 deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" 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> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (count($maps) < 4): ?> <?php if ($isAdmin): ?>
<?php for ($i = 0; $i < 4 - count($maps); ++$i): ?> <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> <div class="mapItem"></div>
<?php endfor; ?> <?php endfor; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
<?php if ($isAdmin): ?>
<script>
(function() {
Maps = {
deleteMap: function(mapId, mapName) {
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
type: 'button',
classNames: ['red'],
text: 'Delete',
onclick: function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
if (this.response.error) {
document.getElementById('loading').style.visibility = 'hidden';
//TODO: handle this error
return;
}
window.location.reload();
});
}
}]);
}
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
for (var button of buttons) {
button.onclick = function() {
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
};
}
})();
</script>
<?php endif; ?>
<?php require ROOT . '/views/templates/main_footer.php'; ?> <?php require ROOT . '/views/templates/main_footer.php'; ?>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="header"> <div class="header">
<h1> <h1>
<a href="/" title="MapGuesser"> <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 MapGuesser
</a> </a>
</h1> </h1>
+14 -3
View File
@@ -1,7 +1,18 @@
<script> <script>
window.addEventListener('beforeunload', function (e) { const STATIC_ROOT = '<?= $_ENV['STATIC_ROOT'] ?>';
document.getElementById('loading').style.visibility = 'visible'; const REVISION = '<?= REVISION ?>';
}); const ANTI_CSRF_TOKEN = '<?= $_SESSION['anti_csrf_token'] ?>';
</script> </script>
<script src="<?= $_ENV['STATIC_ROOT'] ?>/js/mapguesser.js?rev=<?= 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; ?>
</body> </body>
</html> </html>
+18 -7
View File
@@ -4,19 +4,30 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>MapGuesser</title> <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 if (isset($cssFiles)) : ?>
<?php foreach ($cssFiles as $cssFile) : ?> <?php foreach ($cssFiles as $cssFile) : ?>
<?php
if (!preg_match('/^http(s)?/', $cssFile)) {
$cssFile = $_ENV['STATIC_ROOT'] . '/' . $cssFile . '?rev=' . REVISION;
}
?>
<link href="<?= $cssFile ?>" rel="stylesheet"> <link href="<?= $cssFile ?>" rel="stylesheet">
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?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 href="https://fonts.googleapis.com/css2?family=Roboto: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="192x192" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/192x192.png?rev=<?= REVISION ?>">
<link rel="icon" type="image/png" sizes="96x96" href="/static/img/favicon/96x96.png"> <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="/static/img/favicon/32x32.png"> <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="/static/img/favicon/16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="<?= $_ENV['STATIC_ROOT'] ?>/img/favicon/16x16.png?rev=<?= REVISION ?>">
</head> </head>
<body> <body>
<div id="loading"> <div id="loading">
<img src="/static/img/loading.svg"> <img src="<?= $_ENV['STATIC_ROOT'] ?>/img/loading.svg?rev=<?= REVISION ?>">
</div>
<div id="cover"></div>
<div id="modal" class="modal">
<h2 id="modalTitle"></h2>
<p id="modalText" class="marginTop"></p>
<div id="modalButtons" class="right"></div>
</div> </div>
+45
View File
@@ -0,0 +1,45 @@
<?php
require 'main.php';
if (!empty($_ENV['DEV'])) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
ini_set('display_errors', '0');
}
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
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('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.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']);
$routeCollection->post('admin.deleteMap', 'deleteMap/{mapId}', [MapGuesser\Controller\MapAdminController::class, 'deleteMap']);
});
Container::$sessionHandler = new MapGuesser\Session\DatabaseSessionHandler();
session_set_save_handler(Container::$sessionHandler, true);
session_start([
'gc_maxlifetime' => 604800,
'cookie_lifetime' => 604800,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax'
]);
if (!isset($_SESSION['anti_csrf_token'])) {
$_SESSION['anti_csrf_token'] = hash('sha256', random_bytes(10) . microtime());
}