Merged in feature/MAPG-73-implement-map-selection (pull request #58)

Feature/MAPG-73 implement map selection
This commit is contained in:
Bence Pőcze 2020-05-30 15:43:05 +00:00
commit a27aece4fa
10 changed files with 382 additions and 112 deletions

26
USED_SOFTWARE Normal file
View File

@ -0,0 +1,26 @@
---------------------------------------------------------------
Bootstrap Icons
https://github.com/twbs/icons
---------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2019 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---------------------------------------------------------------

View File

@ -3,23 +3,29 @@
require '../main.php';
// very basic routing
$host = $_SERVER["REQUEST_SCHEME"] . '://' . $_SERVER["SERVER_NAME"];
$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'];
$url = $_SERVER['REQUEST_URI'];
if (($pos = strpos($url, '?')) !== false) {
$url = substr($url, 0, $pos);
}
switch($url) {
case '/maps':
$controller = new MapGuesser\Controller\MapsController();
break;
case '/game':
$controller = new MapGuesser\Controller\GameController();
$mapId = isset($_GET['map']) ? (int) $_GET['map'] : 0;
$controller = new MapGuesser\Controller\GameController($mapId);
break;
case '/game.json':
$controller = new MapGuesser\Controller\GameController(true);
$mapId = isset($_GET['map']) ? (int) $_GET['map'] : 0;
$controller = new MapGuesser\Controller\GameController($mapId, true);
break;
case '/position.json':
$controller = new MapGuesser\Controller\PositionController();
$mapId = isset($_GET['map']) ? (int) $_GET['map'] : 0;
$controller = new MapGuesser\Controller\PositionController($mapId);
break;
case '/':
header('Location: ' . $host . '/game', true, 302);
header('Location: ' . $host . '/maps', true, 302);
die;
default:
echo 'Error 404';

View File

@ -12,13 +12,42 @@ html, body {
padding: 0;
}
p, button {
p, h1, h2, button, a {
font-family: 'Roboto', sans-serif;
}
h1, h2 {
font-weight: 500;
}
h1 {
font-size: 32px;
}
h1>a:link, h1>a:visited {
color: inherit;
}
h1>a:hover, h1>a:focus {
text-decoration: none;
}
h2, div.header.small h1 {
font-size: 24px;
}
p, h2 {
line-height: 150%;
}
p {
font-weight: 300;
font-size: 12px;
font-size: 16px;
}
img {
display: block;
max-width: 100%;
}
.mono {
@ -29,7 +58,37 @@ p {
font-weight: 500;
}
button {
.small {
font-size: 12px;
}
.justify {
text-align: justify;
}
.marginTop {
margin-top: 10px;
}
.marginBottom {
margin-bottom: 10px;
}
svg.inline {
vertical-align: -0.15em;
}
a:link, a:visited {
color: #3b5998;
font-weight: 500;
text-decoration: none;
}
a:hover, a:focus {
text-decoration: underline;
}
button, a.button {
cursor: pointer;
font-size: 16px;
font-weight: 500;
@ -39,11 +98,15 @@ button {
height: 35px;
border: none;
border-radius: 3px;
display: inline-block;
text-align: center;
line-height: 35px;
}
button:enabled:hover, button:enabled:focus {
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
background-color: #29457f;
outline: none;
text-decoration: none;
}
button:disabled {
@ -52,33 +115,88 @@ button:disabled {
opacity: 0.7;
}
button.fullWidth {
button.fullWidth, a.button.fullWidth {
padding: 0;
width: 100%;
}
button.gray {
button.gray, a.button.gray {
background-color: #808080;
}
button.gray:hover, button.gray:focus {
button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
background-color: #555555;
}
div.header {
background-color: #333333;
height: 50px;
line-height: 50px;
padding: 0 12px;
color: white;
}
div.header > div.grid {
display: grid;
grid-template-columns: auto auto;
}
div.header.small {
height: 40px;
line-height: 40px;
}
div.main {
padding: 6px 12px;
}
div.buttonContainer {
height: 35px;
}
div.buttonContainer > button {
div.buttonContainer>button {
margin: 0 auto;
}
div.buttonContainer.top {
margin-bottom: 10px;
div.mapContainer {
display: grid;
}
div.buttonContainer.bottom {
margin-top: 10px;
div.mapItem {
width: 375px;
background-color: #eeeeee;
border-radius: 3px;
margin: 10px auto;
}
div.mapItem>div.title {
background-color: #28a745;
color: white;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 4px 8px;
}
div.mapItem>div.title>p.title {
font-weight: 500;
font-size: 18px;
}
div.mapItem>div.inner {
padding: 8px;
}
div.mapItem>div.inner>div.info {
display: grid;
grid-template-columns: auto auto;
}
div.mapItem>div.inner>div.info>p:nth-child(1) {
text-align: left;
}
div.mapItem>div.inner>div.info>p:nth-child(2) {
text-align: right;
}
#loading {
@ -93,26 +211,18 @@ div.buttonContainer.bottom {
}
#roundInfo {
position: absolute;
top: 5px;
left: 5px;
height: 28px;
line-height: 28px;
padding: 0 8px;
background-color: #eeeeee;
border: solid 1px #555555;
border-radius: 3px;
opacity: 0.95;
z-index: 2;
line-height: inherit;
text-align: right;
}
#roundInfo p {
font-size: 16px;
line-height: inherit;
}
#panorama {
height: 100%;
width: 100%;
height: calc(100% - 40px);
z-index: 1;
}
@ -123,16 +233,15 @@ div.buttonContainer.bottom {
z-index: 2;
}
#guess > #continueButtonContainer {
#guess>#continueButtonContainer {
display: none;
}
#guess.result > #closeGuessButtonContainer,
#guess.result > #guessButtonContainer {
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
display: none;
}
#guess.result > #continueButtonContainer {
#guess.result>#continueButtonContainer {
display: block;
}
@ -141,14 +250,14 @@ div.buttonContainer.bottom {
border-radius: 3px;
}
#guess.result > #map {
#guess.result>#map {
height: calc(100% - 170px);
}
#resultInfo {
margin-top: 5px;
height: 120px;
width: 100%;
height: 120px;
padding: 5px 20px;
text-align: center;
box-sizing: border-box;
@ -157,13 +266,13 @@ div.buttonContainer.bottom {
display: none;
}
#guess.result > #resultInfo {
#guess.result>#resultInfo {
display: block;
}
#resultInfo > div {
height: 33.33%;
#resultInfo>div {
width: 100%;
height: 33.33%;
display: flex;
justify-content: center;
align-items: center;
@ -171,9 +280,10 @@ div.buttonContainer.bottom {
#resultInfo p {
font-size: 24px;
line-height: 1;
}
#distanceInfo > p:nth-child(2), #scoreInfo > p:nth-child(2) {
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
display: none;
}
@ -185,8 +295,8 @@ div.buttonContainer.bottom {
}
#scoreBar {
height: 100%;
width: 0;
height: 100%;
border-radius: 3px;
transition-property: width;
transition-duration: 2.0s;
@ -196,12 +306,38 @@ div.buttonContainer.bottom {
display: none;
}
@media screen and (min-width: 1600px) {
div.mapContainer {
grid-template-columns: auto auto auto auto;
}
}
@media screen and (min-width: 1200px) and (max-width: 1599px) {
div.mapContainer {
grid-template-columns: auto auto auto;
}
}
@media screen and (min-width: 800px) and (max-width: 1199px) {
div.mapContainer {
grid-template-columns: auto auto;
}
}
@media screen and (max-width: 799px) {
div.mapContainer {
grid-template-columns: auto;
}
}
@media screen and (max-width: 599px) {
div.header.small h1 span {
display: none;
}
button {
padding: 0;
width: 100%;
}
#showGuessButtonContainer {
position: absolute;
left: 20px;
@ -209,18 +345,15 @@ div.buttonContainer.bottom {
right: 20px;
z-index: 2;
}
#guess {
left: 20px;
top: 40px;
top: 50px;
opacity: 0.95;
visibility: hidden;
}
#map {
height: calc(100% - 90px);
}
#scoreBarBase {
width: 100%;
}
@ -230,13 +363,11 @@ div.buttonContainer.bottom {
#showGuessButtonContainer {
display: none;
}
#guess {
width: 500px;
height: 375px;
opacity: 0.95;
}
#guess.adapt {
top: initial;
width: 250px;
@ -246,46 +377,38 @@ div.buttonContainer.bottom {
transition-duration: 0.1s;
transition-delay: 0.8s;
}
#guess.adapt:hover {
width: 500px;
height: 375px;
opacity: 0.95;
transition-delay: 0s;
}
#closeGuessButtonContainer {
display: none;
}
#map {
height: calc(100% - 45px);
}
#guess.result {
width: initial;
height: initial;
top: 40px;
top: 50px;
left: 50px;
right: 50px;
bottom: 50px;
}
#scoreBarBase {
width: 60%;
}
@media screen and (max-height: 424px) {
#guess {
top: 40px;
top: 50px;
height: initial;
}
#guess.adapt:hover {
top: 40px;
top: 50px;
height: initial;
}
#guess.result {
left: 20px;
right: 20px;

View File

@ -51,7 +51,7 @@
Core.startNewRound();
};
xhr.open('GET', 'position.json', true);
xhr.open('GET', 'position.json?map=' + mapId, true);
xhr.send();
},
@ -127,7 +127,7 @@
Core.resetGame();
};
xhr.open('GET', 'game.json', true);
xhr.open('GET', 'game.json?map=' + mapId, true);
xhr.send();
},
@ -200,7 +200,7 @@
Core.panoId = this.response.panoId;
};
xhr.open('POST', 'position.json', true);
xhr.open('POST', 'position.json?map=' + mapId, true);
xhr.send(data);
},

View File

@ -10,13 +10,13 @@ use MapGuesser\Interfaces\View\IView;
class GameController implements IController
{
private int $mapId;
private bool $jsonResponse;
// demo map
private int $mapId = 1;
public function __construct($jsonResponse = false)
public function __construct(int $mapId, $jsonResponse = false)
{
$this->mapId = $mapId;
$this->jsonResponse = $jsonResponse;
}
@ -32,7 +32,7 @@ class GameController implements IController
];
}
$data = ['bounds' => $bounds->toArray()];
$data = ['mapId' => $this->mapId, 'bounds' => $bounds->toArray()];
if ($this->jsonResponse) {
return new JsonView($data);

View File

@ -0,0 +1,60 @@
<?php
namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Database\RawExpression;
use MapGuesser\Interfaces\Controller\IController;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\View\IView;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\View\HtmlView;
class MapsController implements IController
{
public function run(): IView
{
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],
['maps', 'name'],
['maps', 'description'],
['maps', 'bound_south_lat'],
['maps', 'bound_west_lng'],
['maps', 'bound_north_lat'],
['maps', 'bound_east_lng'],
new RawExpression('COUNT(places.id) AS num_places')
]);
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
$select->orderBy('name');
$result = $select->execute();
$maps = [];
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->formatMapArea($bounds->calculateApproximateArea());
$maps[] = $map;
}
$data = ['maps' => $maps];
return new HtmlView('maps', $data);
}
private function formatMapArea(float $area): string
{
//TODO: this should be formatted more properly
if ($area < 100000.0) {
return round($area, 0) . ' m^2';
} elseif ($area < 100000000.0) {
return round($area / 1000000.0, 0) . ' km^2';
} elseif ($area < 10000000000.0) {
return round($area / 1000000.0, -2) . ' km^2';
} else {
return round($area / 1000000.0, -4) . ' km^2';
}
}
}

View File

@ -13,8 +13,12 @@ class PositionController implements IController
const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000;
// demo map
private int $mapId = 1;
private int $mapId;
public function __construct(int $mapId)
{
$this->mapId = $mapId;
}
public function run(): IView
{
@ -120,7 +124,7 @@ class PositionController implements IController
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $this->mapId);
$numberOfPlaces = $select->count();
$numberOfPlaces = $select->count();// TODO: what if 0
$randomOffset = random_int(0, $numberOfPlaces - 1);
$select->orderBy('id');

View File

@ -4,21 +4,25 @@ class Bounds
{
const ONE_DEGREE_OF_LATITUDE_IN_METER = 111132.954;
private float $southLat;
private float $westLng;
private float $southLat = 90.0;
private float $westLng = 180.0;
private float $northLat;
private float $eastLng;
private float $northLat = -90.0;
private float $eastLng = -180.0;
private bool $initialized = false;
public static function createWithPosition(Position $position): Bounds
public function __construct(Position $position = null)
{
$instance = new static();
if ($position === null) {
return;
}
$instance->initialize($position);
$lat = $position->getLat();
$lng = $position->getLng();
return $instance;
$this->northLat = $lat;
$this->westLng = $lng;
$this->southLat = $lat;
$this->eastLng = $lng;
}
public static function createDirectly(float $southLat, float $westLng, float $northLat, float $eastLng): Bounds
@ -30,19 +34,11 @@ class Bounds
$instance->northLat = $northLat;
$instance->eastLng = $eastLng;
$instance->initialized = true;
return $instance;
}
public function extend(Position $position): void
{
if (!$this->initialized) {
$this->initialize($position);
return;
}
$lat = $position->getLat();
$lng = $position->getLng();
@ -77,10 +73,6 @@ class Bounds
public function toArray(): array
{
if (!$this->initialized) {
throw new \Exception("Bounds are not initialized!");
}
return [
'south' => $this->southLat,
'west' => $this->westLng,
@ -93,17 +85,4 @@ class Bounds
{
return json_encode($this->toArray());
}
private function initialize(Position $position)
{
$lat = $position->getLat();
$lng = $position->getLng();
$this->northLat = $lat;
$this->westLng = $lng;
$this->southLat = $lat;
$this->eastLng = $lng;
$this->initialized = true;
}
}

View File

@ -8,22 +8,33 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&family=Roboto+Mono:wght@300;500&display=swap" rel="stylesheet">
</head>
<body>
<div class="header small">
<div class="grid">
<h1>
<a href="maps" title="Back to playable maps">
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" />
</svg>
<span>MapGuesser</span>
</a>
</h1>
<p id="roundInfo">Round: <span id="currentRound" class="mono bold"></span> | Score: <span id="currentScoreSum" class="mono bold"></span></p>
</div>
</div>
<div id="loading">
<img src="static/img/loading.svg">
</div>
<div id="roundInfo">
<p>Round: <span id="currentRound" class="mono bold"></span> | Score: <span id="currentScoreSum" class="mono bold"></span></p>
</div>
<div id="panorama"></div>
<div id="showGuessButtonContainer">
<button id="showGuessButton" class="fullWidth">Show guess map</button>
</div>
<div id="guess">
<div id="closeGuessButtonContainer" class="buttonContainer top">
<div id="closeGuessButtonContainer" class="buttonContainer marginBottom">
<button id="closeGuessButton" class="fullWidth gray">Close</button>
</div>
<div id="map"></div>
<div id="guessButtonContainer" class="buttonContainer bottom">
<div id="guessButtonContainer" class="buttonContainer marginTop">
<button id="guessButton" class="fullWidth" disabled>Guess</button>
</div>
<div id="resultInfo">
@ -41,16 +52,17 @@
</div>
</div>
</div>
<div id="continueButtonContainer" class="buttonContainer bottom">
<div id="continueButtonContainer" class="buttonContainer marginTop">
<button id="continueButton" class="fullWidth">Continue</button>
<button id="showSummaryButton" class="fullWidth">Show summary</button>
<button id="startNewGameButton" class="fullWidth">Start new game</button>
<button id="startNewGameButton" class="fullWidth">Play this map again</button>
</div>
</div>
<script>
var mapId = '<?= $mapId ?>';
var mapBounds = <?= json_encode($bounds) ?>;
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>"></script>
<script src="static/js/mapguesser.js"></script>
<script src="static/js/game.js"></script>
</body>
</html>

60
views/maps.php Normal file
View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MapGuesser</title>
<link href="static/css/mapguesser.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&family=Roboto+Mono:wght@300;500&display=swap" rel="stylesheet">
</head>
<body>
<div class="header">
<h1>
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" />
</svg>
MapGuesser
</h1>
</div>
<div class="main">
<h2>Playable maps</h2>
<div class="mapContainer">
<?php foreach ($maps as $map) : ?>
<div class="mapItem">
<div class="title">
<p class="title"><?= $map['name'] ?></p>
</div>
<img src="https://maps.googleapis.com/maps/api/staticmap?size=375x200&visible=<?= $map['bound_south_lat'] . ',' . $map['bound_west_lng'] . '|' . $map['bound_north_lat'] . ',' . $map['bound_east_lng'] ?>&key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>" width="375" height="200">
<div class="inner">
<div class="info">
<p>
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" />
</svg>
<?= $map['num_places'] ?> places
</p>
<p>
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.5 2h-9V1h9v1zm-10 1.5v9h-1v-9h1zm11 9v-9h1v9h-1zM3.5 14h9v1h-9v-1z" />
<path fill-rule="evenodd" d="M14 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM2 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4z" />
</svg>
~<?= $map['area'] ?>
</p>
</div>
<p class="small justify marginTop"><?= $map['description'] ?></p>
</div>
<a class="button fullWidth" href="game?map=<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
</div>
<?php endforeach; ?>
<?php if (count($maps) < 4): ?>
<?php for ($i = 0; $i < 4 - count($maps); ++$i): ?>
<div class="mapItem"></div>
<?php endfor; ?>
<?php endif; ?>
</div>
</div>
</body>
</html>