diff --git a/USED_SOFTWARE b/USED_SOFTWARE new file mode 100644 index 0000000..6132122 --- /dev/null +++ b/USED_SOFTWARE @@ -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. +--------------------------------------------------------------- \ No newline at end of file diff --git a/public/index.php b/public/index.php index aa06a8a..0d592cc 100644 --- a/public/index.php +++ b/public/index.php @@ -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'; diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index 2d09895..1effcbd 100644 --- a/public/static/css/mapguesser.css +++ b/public/static/css/mapguesser.css @@ -12,13 +12,61 @@ html, body { padding: 0; } -p, button { +button::-moz-focus-inner, input::-moz-focus-inner { + padding: 0; + border: 0; +} + +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; +} + +sup, sub { + position: relative; + font-size: smaller; +} + +sup { + vertical-align: top; + top: -0.4em; +} + +sub { + vertical-align: bottom; + bottom: -0.4em; } .mono { @@ -29,7 +77,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 +117,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 +134,92 @@ 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: 350px; + 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>img { + width: 100%; +} + +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 { @@ -89,33 +230,36 @@ div.buttonContainer.bottom { left: 50%; margin-top: -32px; margin-left: -32px; - z-index: 3; + z-index: 5; } #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; } +#cover { + position: absolute; + top: 40px; + left: 0; + bottom: 0; + right: 0; + background-color: #000000; + opacity: 0.5; + z-index: 3; +} + #guess { position: absolute; bottom: 30px; @@ -123,16 +267,19 @@ div.buttonContainer.bottom { z-index: 2; } -#guess > #continueButtonContainer { +#guess.result { + z-index: 4; +} + +#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 +288,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 +304,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 +318,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 +333,8 @@ div.buttonContainer.bottom { } #scoreBar { - height: 100%; width: 0; + height: 100%; border-radius: 3px; transition-property: width; transition-duration: 2.0s; @@ -196,12 +344,44 @@ div.buttonContainer.bottom { display: none; } +@media screen and (min-width: 1504px) { + div.mapContainer { + grid-template-columns: auto auto auto auto; + } +} + +@media screen and (min-width: 1134px) and (max-width: 1503px) { + div.mapContainer { + grid-template-columns: auto auto auto; + } +} + +@media screen and (min-width: 764px) and (max-width: 1133px) { + div.mapContainer { + grid-template-columns: auto auto; + } +} + +@media screen and (max-width: 763px) { + div.mapContainer { + grid-template-columns: auto; + } +} + +@media screen and (max-width: 374px) { + div.mapItem { + width: initial; + } +} + @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 +389,15 @@ div.buttonContainer.bottom { right: 20px; z-index: 2; } - #guess { + top: 50px; left: 20px; - top: 40px; opacity: 0.95; visibility: hidden; } - #map { height: calc(100% - 90px); } - #scoreBarBase { width: 100%; } @@ -230,13 +407,11 @@ div.buttonContainer.bottom { #showGuessButtonContainer { display: none; } - #guess { width: 500px; height: 375px; opacity: 0.95; } - #guess.adapt { top: initial; width: 250px; @@ -246,46 +421,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; diff --git a/public/static/js/mapguesser.js b/public/static/js/game.js similarity index 94% rename from public/static/js/mapguesser.js rename to public/static/js/game.js index e31507e..a8084c8 100644 --- a/public/static/js/mapguesser.js +++ b/public/static/js/game.js @@ -16,6 +16,7 @@ initialize: function () { document.getElementById('loading').style.visibility = 'visible'; + document.getElementById('cover').style.visibility = 'visible'; document.getElementById('currentRound').innerHTML = '1/' + String(Core.NUMBER_OF_ROUNDS); document.getElementById('currentScoreSum').innerHTML = '0/0'; @@ -33,6 +34,7 @@ } document.getElementById('loading').style.visibility = 'hidden'; + document.getElementById('cover').style.visibility = 'hidden'; Core.panoId = this.response.panoId; @@ -51,11 +53,16 @@ Core.startNewRound(); }; - xhr.open('GET', 'position.json', true); + xhr.open('GET', 'position.json?map=' + mapId, true); xhr.send(); }, resetGame: function () { + if (Core.guessMarker) { + Core.guessMarker.setMap(null); + Core.guessMarker = null; + } + for (var i = 0; i < Core.rounds.length; ++i) { var round = Core.rounds[i]; @@ -96,6 +103,7 @@ lastRound.line.setVisible(false); } + document.getElementById('cover').style.visibility = 'hidden'; document.getElementById('showGuessButton').style.visibility = null; document.getElementById('guess').style.visibility = null; document.getElementById('guess').classList.remove('result') @@ -127,7 +135,7 @@ Core.resetGame(); }; - xhr.open('GET', 'game.json', true); + xhr.open('GET', 'game.json?map=' + mapId, true); xhr.send(); }, @@ -149,6 +157,7 @@ document.getElementById('guess').classList.remove('adapt'); } document.getElementById('loading').style.visibility = 'visible'; + document.getElementById('cover').style.visibility = 'visible'; var data = new FormData(); data.append('guess', '1'); @@ -192,7 +201,7 @@ scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.width = scoreBarProperties.width; - if (Core.rounds.length == Core.NUMBER_OF_ROUNDS) { + if (Core.rounds.length === Core.NUMBER_OF_ROUNDS) { document.getElementById('continueButton').style.display = 'none'; document.getElementById('showSummaryButton').style.display = 'block'; } @@ -200,7 +209,7 @@ Core.panoId = this.response.panoId; }; - xhr.open('POST', 'position.json', true); + xhr.open('POST', 'position.json?map=' + mapId, true); xhr.send(data); }, @@ -438,7 +447,13 @@ Core.resetGame(); } + // showing the loading animation is not possible, because we don't know if user cancelled the leave + window.onbeforeunload = function (e) { + if (Core.rounds[Core.rounds.length - 1].position) { + return; + } + e.preventDefault(); e.returnValue = ''; }; diff --git a/src/Controller/GameController.php b/src/Controller/GameController.php index 9f38af2..c1174f4 100644 --- a/src/Controller/GameController.php +++ b/src/Controller/GameController.php @@ -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); diff --git a/src/Controller/MapsController.php b/src/Controller/MapsController.php new file mode 100644 index 0000000..569d667 --- /dev/null +++ b/src/Controller/MapsController.php @@ -0,0 +1,66 @@ +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->formatMapAreaForHuman($bounds->calculateApproximateArea()); + + $maps[] = $map; + } + + $data = ['maps' => $maps]; + return new HtmlView('maps', $data); + } + + private function formatMapAreaForHuman(float $area): array + { + if ($area < 100000.0) { + $digits = 0; + $rounded = round($area, 0); + $unit = 'm'; + } elseif ($area < 100000000.0) { + $digits = 0; + $rounded = round($area / 1000000.0, 0); + $unit = 'km'; + } elseif ($area < 10000000000.0) { + $digits = 0; + $rounded = round($area / 1000000.0, -2); + $unit = 'km'; + } else { + $digits = 0; + $rounded = round($area / 1000000.0, -4); + $unit = 'km'; + } + + return [number_format($rounded, $digits, '.', ' '), $unit]; + } +} diff --git a/src/Controller/PositionController.php b/src/Controller/PositionController.php index eca8311..cddee8d 100644 --- a/src/Controller/PositionController.php +++ b/src/Controller/PositionController.php @@ -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'); diff --git a/src/Util/Geo/Bounds.php b/src/Util/Geo/Bounds.php index 7b448be..1edfb8b 100644 --- a/src/Util/Geo/Bounds.php +++ b/src/Util/Geo/Bounds.php @@ -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; - } } diff --git a/views/game.php b/views/game.php index fee7284..1e17b03 100644 --- a/views/game.php +++ b/views/game.php @@ -11,19 +11,31 @@
Round: | Score:
+Round: | Score:
+