Merged in develop (pull request #12)

Develop
This commit is contained in:
Bence Pőcze 2020-05-20 14:01:41 +00:00
commit d1b0522fdd
25 changed files with 703 additions and 171 deletions

View File

@ -1,2 +1,6 @@
DEV=true DEV=1
DB_HOST=mariadb
DB_USER=mapguesser
DB_PASSWORD=mapguesser
DB_NAME=mapguesser
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.gif filter=lfs diff=lfs merge=lfs -text

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.env .env
installed
vendor vendor

27
db/mapguesser.sql Normal file
View File

@ -0,0 +1,27 @@
SET NAMES utf8mb4;
SET foreign_key_checks = 0;
DROP TABLE IF EXISTS `maps`;
CREATE TABLE `maps` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` text NOT NULL,
`bound_south_lat` decimal(8,6) NOT NULL,
`bound_west_lng` decimal(9,6) NOT NULL,
`bound_north_lat` decimal(8,6) NOT NULL,
`bound_east_lng` decimal(9,6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `places`;
CREATE TABLE `places` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`map_id` int(10) unsigned NOT NULL,
`lat` decimal(8,6) NOT NULL,
`lng` decimal(9,6) NOT NULL,
PRIMARY KEY (`id`),
KEY `map_id` (`map_id`),
CONSTRAINT `places_map_id` FOREIGN KEY (`map_id`) REFERENCES `maps` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -13,7 +13,6 @@ services:
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
environment: environment:
#TZ: Europe/Budapest
MYSQL_ROOT_PASSWORD: 'root' MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'mapguesser' MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser' MYSQL_USER: 'mapguesser'

View File

@ -4,13 +4,9 @@ ENV DEBIAN_FRONTEND noninteractive
# Install Apache, PHP and further necessary packages # Install Apache, PHP and further necessary packages
RUN apt update RUN apt update
RUN apt install -y curl git apache2 \ RUN apt install -y curl git mariadb-client apache2 \
php-apcu php-xdebug php7.4-cli php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip php-apcu php-xdebug php7.4-cli php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
# Configure tzdata
#RUN ln -fs /usr/share/zoneinfo/Europe/Budapest /etc/localtime
#RUN dpkg-reconfigure --frontend noninteractive tzdata
# Configure Apache with PHP # Configure Apache with PHP
RUN mkdir -p /run/php RUN mkdir -p /run/php
RUN a2enmod proxy_fcgi rewrite RUN a2enmod proxy_fcgi rewrite
@ -24,6 +20,12 @@ RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.
COPY scripts/install-composer.sh install-composer.sh COPY scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh RUN ./install-composer.sh
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
RUN npm install -g uglify-js
RUN npm install -g clean-css-cli
EXPOSE 80 EXPOSE 80
VOLUME /var/www/mapguesser VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser WORKDIR /var/www/mapguesser

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
EXPECTED_CHECKSUM="$(curl -s https://composer.github.io/installer.sig)" EXPECTED_CHECKSUM="$(curl -sL https://composer.github.io/installer.sig)"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"

View File

@ -2,7 +2,9 @@
require 'vendor/autoload.php'; require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); const ROOT = __DIR__;
$dotenv = Dotenv\Dotenv::createImmutable(ROOT);
$dotenv->load(); $dotenv->load();
if (!empty($_ENV['DEV'])) { if (!empty($_ENV['DEV'])) {

5
public/.htaccess Normal file
View File

@ -0,0 +1,5 @@
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]

View File

@ -2,39 +2,26 @@
require '../main.php'; require '../main.php';
// demo position // very basic routing
$realPosition = new MapGuesser\Geo\Position(47.85239, 13.35101); $host = $_SERVER["REQUEST_SCHEME"] . '://' . $_SERVER["SERVER_NAME"];
$url = $_SERVER['REQUEST_URI'];
switch($url) {
case '/game':
$controller = new MapGuesser\Controller\GameController();
break;
case '/getNewPosition.json':
$controller = new MapGuesser\Controller\GetNewPosition();
break;
case '/':
header('Location: ' . $host . '/game', true, 302);
die;
default:
echo 'Error 404';
die;
}
// demo bounds $view = $controller->run();
$bounds = new MapGuesser\Geo\Bounds($realPosition);
$bounds->extend(new MapGuesser\Geo\Position(48.07683,7.35758));
$bounds->extend(new MapGuesser\Geo\Position(47.57496, 19.08077));
?> header('Content-Type: ' . $view->getContentType() . '; charset=UTF-8');
<!DOCTYPE html> echo $view->render();
<html>
<head>
<meta charset="utf-8">
<title>MapGuesser</title>
<link rel="stylesheet" type="text/css" href="static/css/mapguesser.css">
</head>
<body>
<div id="panorama"></div>
<div id="guess">
<div id="guessMap"></div>
<div id="guessButtonContainer">
<button id="guessButton" disabled>Guess</button>
</div>
</div>
<script>
var realPosition = <?= $realPosition->toJson() ?>;
var guessMapBounds = <?= $bounds->toJson() ?>;
</script>
<script src="static/js/mapguesser.js" async defer></script>
<script src="https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>&callback=initialize" async defer></script>
</body>
</html>

View File

@ -1,9 +1,65 @@
* {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
vertical-align: baseline;
}
html, body { html, body {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
p, button {
font-family: 'Roboto', sans-serif;
}
p {
font-weight: 300;
font-size: 12px;
}
.bold {
font-weight: 500;
}
button {
cursor: pointer;
font-size: 15px;
font-weight: 500;
color: #ffffff;
background-color: #5e77aa;
padding: 8px 15px;
border: none;
border-radius: 3px;
}
button:hover, button:focus {
background-color: #29457f;
outline: none;
}
button:disabled {
cursor: no-drop;
color: #dddddd;
background-color: #808080;
opacity: 0.7;
}
#loading {
position: absolute;
width: 40px;
height: 40px;
top: 50%;
left: 50%;
margin-top: -20px;
margin-left: -20px;
z-index: 2;
visibility: visible;
}
#panorama { #panorama {
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -18,6 +74,7 @@ html, body {
height: 150px; height: 150px;
opacity: 0.5; opacity: 0.5;
z-index: 2; z-index: 2;
visibility: visible;
transition-property: width, height, opacity; transition-property: width, height, opacity;
transition-duration: 0.1s; transition-duration: 0.1s;
transition-delay: 0.8s; transition-delay: 0.8s;
@ -26,11 +83,11 @@ html, body {
#guess:hover { #guess:hover {
width: 500px; width: 500px;
height: 350px; height: 350px;
opacity: 1.0; opacity: 0.95;
transition-delay: 0s; transition-delay: 0s;
} }
#guess #guessMap { #guess > #guessMap {
height: 115px; height: 115px;
width: 100%; width: 100%;
transition-property: height; transition-property: height;
@ -39,7 +96,7 @@ html, body {
border-radius: 3px; border-radius: 3px;
} }
#guess:hover #guessMap { #guess:hover > #guessMap {
height: 315px; height: 315px;
transition-delay: 0s; transition-delay: 0s;
} }
@ -50,26 +107,61 @@ html, body {
} }
#guessButton { #guessButton {
cursor: pointer; padding: 0;
font-size: 14px;
font-weight: bold;
color: #ffffff;
background-color: #5e77aa;
border: none;
border-radius: 3px;
width: 100%; width: 100%;
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
#guessButton:hover, #guessButton:focus { #result {
background-color: #29457f; position: absolute;
outline: none; top: 50px;
left: 50px;
right: 50px;
bottom: 50px;
opacity: 0.95;
z-index: 2;
visibility: hidden;
background-color: #ffffff;
border-radius: 3px;
} }
#guessButton:disabled { #resultMap {
cursor: no-drop; height: 70%;
color: #dddddd; width: 100%;
background-color: #808080; }
opacity: 0.7;
#resultInfo {
height: 30%;
width: 100%;
padding: 10px 20px;
text-align: center;
box-sizing: border-box;
}
#resultInfo > div {
height: 25%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#resultInfo p {
font-size: 24px;
}
#scoreBarBase {
height: 20px;
width: 60%;
margin: 0 auto;
background-color: #eeeeee;
border-radius: 3px;
}
#scoreBar {
height: 100%;
width: 0;
transition-property: width;
transition-duration: 2.0s;
} }

BIN
public/static/img/loading.gif (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,108 +1,248 @@
Math.deg2rad = function (deg) { (function () {
return deg * (this.PI / 180.0); var Core = {
}; MAX_SCORE: 1000,
var Util = { realPosition: null,
EARTH_RADIUS_IN_METER: 6371000, panorama: null,
guessMap: null,
guessMarker: null,
resultMap: null,
resultMarkers: { guess: null, real: null },
googleLink: null,
calculateDistance: function (position1, position2) { getNewPosition: function () {
var lat1 = Math.deg2rad(position1.lat); Core.panorama.setVisible(false);
var lng1 = Math.deg2rad(position1.lng);
var lat2 = Math.deg2rad(position2.lat);
var lng2 = Math.deg2rad(position2.lng);
var latDelta = lat2 - lat1; document.getElementById('loading').style.visibility = 'visible';
var lonDelta = lng2 - lng1;
var angle = 2 * Math.asin( var xhr = new XMLHttpRequest();
Math.sqrt( xhr.responseType = 'json';
Math.pow(Math.sin(latDelta / 2), 2) + xhr.onreadystatechange = function () {
Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(lonDelta / 2), 2) if (this.readyState == 4 && this.status == 200) {
) Core.realPosition = this.response.position;
);
return angle * Util.EARTH_RADIUS_IN_METER; var sv = new google.maps.StreetViewService();
sv.getPanorama({ location: this.response.position, preference: google.maps.StreetViewPreference.NEAREST }, Core.loadPano);
} }
}; };
xhr.open('GET', 'getNewPosition.json', true);
xhr.send();
},
loadPano: function (data, status) {
if (status !== google.maps.StreetViewStatus.OK) {
Core.getNewPosition();
return;
}
document.getElementById('loading').style.visibility = 'hidden';
Core.panorama.setVisible(true);
Core.panorama.setPov({ heading: 0, pitch: 0, zoom: 1 });
Core.panorama.setPano(data.location.pano);
},
calculateScore: function (distance) {
var goodness = 1.0 - distance / Math.sqrt(mapArea);
return Math.pow(Core.MAX_SCORE, goodness);
},
calculateScoreBarProperties: function (score) {
var percent = Math.ceil((score / Core.MAX_SCORE) * 100);
var color;
if (percent >= 90) {
color = '#11ca00';
} else if (percent >= 10) {
color = '#ea9000';
} else {
color = '#ca1100';
}
return { width: percent + '%', backgroundColor: color };
},
var MapManipulator = {
rewriteGoogleLink: function () { rewriteGoogleLink: function () {
if (!googleLink) { if (!Core.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) {
googleLink = a; Core.googleLink = a;
break; break;
} }
} }
} }
setTimeout(function () { setTimeout(function () {
googleLink.title = 'Google Maps' Core.googleLink.title = 'Google Maps'
googleLink.href = 'https://maps.google.com/maps' Core.googleLink.href = 'https://maps.google.com/maps'
}, 1); }, 1);
} }
}; };
var panorama; var Util = {
var guessMap; EARTH_RADIUS_IN_METER: 6371000,
var guessMarker;
var googleLink;
function initialize() { deg2rad: function (deg) {
panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), { return deg * (Math.PI / 180.0);
position: realPosition,
pov: {
heading: 34,
pitch: 10
}, },
disableDefaultUI: true,
linksControl: true,
showRoadLabels: false
});
panorama.addListener('position_changed', function () { calculateDistance: function (position1, position2) {
MapManipulator.rewriteGoogleLink(); var lat1 = Util.deg2rad(position1.lat);
}); var lng1 = Util.deg2rad(position1.lng);
var lat2 = Util.deg2rad(position2.lat);
var lng2 = Util.deg2rad(position2.lng);
panorama.addListener('pov_changed', function () { var angleCos = Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1) +
MapManipulator.rewriteGoogleLink(); Math.sin(lat1) * Math.sin(lat2);
});
guessMap = new google.maps.Map(document.getElementById('guessMap'), { if (angleCos > 1.0) {
angleCos = 1.0;
}
var angle = Math.acos(angleCos);
return angle * Util.EARTH_RADIUS_IN_METER;
},
printDistanceForHuman: function (distance) {
if (distance < 1000) {
return Number.parseFloat(distance).toFixed(0) + ' m';
} else if (distance < 10000) {
return Number.parseFloat(distance / 1000).toFixed(2) + ' km';
} else if (distance < 100000) {
return Number.parseFloat(distance / 1000).toFixed(1) + ' km';
} else {
return Number.parseFloat(distance / 1000).toFixed(0) + ' km';
}
}
};
Core.guessMap = new google.maps.Map(document.getElementById('guessMap'), {
disableDefaultUI: true, disableDefaultUI: true,
clickableIcons: false, clickableIcons: false,
draggableCursor: 'crosshair' draggableCursor: 'crosshair'
}); });
guessMap.fitBounds(guessMapBounds); Core.guessMap.fitBounds(guessMapBounds);
guessMap.addListener('click', function (e) { Core.guessMap.addListener('click', function (e) {
if (guessMarker) { if (Core.guessMarker) {
guessMarker.setPosition(e.latLng); Core.guessMarker.setPosition(e.latLng);
return; return;
} }
guessMarker = new google.maps.Marker({ Core.guessMarker = new google.maps.Marker({
map: guessMap, map: Core.guessMap,
position: e.latLng, position: e.latLng,
draggable: true clickable: false,
draggable: true,
label: {
color: '#ffffff',
fontFamily: 'Roboto',
fontSize: '18px',
fontWeight: '500',
text: '?'
}
}); });
document.getElementById('guessButton').disabled = false; document.getElementById('guessButton').disabled = false;
}); });
}
Core.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
disableDefaultUI: true,
linksControl: true,
showRoadLabels: false
});
Core.panorama.addListener('position_changed', function () {
Core.rewriteGoogleLink();
});
Core.panorama.addListener('pov_changed', function () {
Core.rewriteGoogleLink();
});
Core.resultMap = new google.maps.Map(document.getElementById('resultMap'), {
disableDefaultUI: true,
clickableIcons: false,
});
Core.getNewPosition();
document.getElementById('guessButton').onclick = function () { document.getElementById('guessButton').onclick = function () {
if (!guessMarker) { if (!Core.guessMarker) {
return; return;
} }
var guessedPosition = guessMarker.getPosition(); var guessedPosition = Core.guessMarker.getPosition();
var distance = Util.calculateDistance(realPosition, { lat: guessedPosition.lat(), lng: guessedPosition.lng() });
alert('You were ' + distance + 'm close!'); this.disabled = true;
Core.guessMarker.setMap(null);
Core.guessMarker = null;
this.blur(); var distance = Util.calculateDistance(Core.realPosition, { lat: guessedPosition.lat(), lng: guessedPosition.lng() });
document.getElementById('guess').style.visibility = 'hidden';
document.getElementById('result').style.visibility = 'visible';
var resultBounds = new google.maps.LatLngBounds();
resultBounds.extend(Core.realPosition);
resultBounds.extend(guessedPosition);
Core.resultMap.fitBounds(resultBounds);
Core.resultMarkers.real = new google.maps.Marker({
map: Core.resultMap,
position: Core.realPosition,
clickable: true,
draggable: false
});
Core.resultMarkers.guess = new google.maps.Marker({
map: Core.resultMap,
position: guessedPosition,
clickable: false,
draggable: false,
label: {
color: '#ffffff',
fontFamily: 'Roboto',
fontSize: '18px',
fontWeight: '500',
text: '?'
} }
});
Core.resultMarkers.real.addListener('click', function () {
window.open('https://www.google.com/maps/search/?api=1&query=' + Core.realPosition.lat + ',' + Core.realPosition.lng, '_blank');
});
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(distance);
var score = Core.calculateScore(distance);
var scoreBarProperties = Core.calculateScoreBarProperties(score);
document.getElementById('score').innerHTML = Number.parseFloat(score).toFixed(0);
var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width;
}
document.getElementById('continueButton').onclick = function () {
document.getElementById('scoreBar').style.width = '0';
Core.resultMarkers.real.setMap(null);
Core.resultMarkers.real = null;
Core.resultMarkers.guess.setMap(null);
Core.resultMarkers.guess = null;
document.getElementById('guess').style.visibility = 'visible';
document.getElementById('result').style.visibility = 'hidden';
Core.guessMap.fitBounds(guessMapBounds);
Core.getNewPosition();
}
})();

23
scripts/install.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -f ${ROOT_DIR}/installed ]; then
echo "Mapguesser is already installed! To force reinstall, delete file 'installed' from the root directory!"
exit 1
fi
echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Uglifying JS and CSS files..."
uglifyjs ${ROOT_DIR}/public/static/js/mapguesser.js -c -m -o ${ROOT_DIR}/public/static/js/mapguesser.js
cleancss ${ROOT_DIR}/public/static/css/mapguesser.css -o ${ROOT_DIR}/public/static/css/mapguesser.css
fi
touch ${ROOT_DIR}/installed

12
scripts/update.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Uglifying JS and CSS files..."
uglifyjs ${ROOT_DIR}/public/static/js/mapguesser.js -c -m -o ${ROOT_DIR}/public/static/js/mapguesser.js
cleancss ${ROOT_DIR}/public/static/css/mapguesser.css -o ${ROOT_DIR}/public/static/css/mapguesser.css
fi

View File

@ -0,0 +1,8 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\View\ViewBase;
interface ControllerInterface
{
public function run(): ViewBase;
}

View File

@ -0,0 +1,35 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Util\Geo\Position;
use MapGuesser\View\HtmlView;
use MapGuesser\View\ViewBase;
use mysqli;
class GameController implements ControllerInterface
{
public function run(): ViewBase
{
$mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
// demo map
$mapId = 1;
$stmt = $mysql->prepare('SELECT bound_south_lat, bound_west_lng, bound_north_lat, bound_east_lng FROM maps WHERE id=?');
$stmt->bind_param("i", $mapId);
$stmt->execute();
$map = $stmt->get_result()->fetch_assoc();
// using RAND() for the time being, could be changed in the future
$stmt = $mysql->prepare('SELECT lat, lng FROM places WHERE map_id=? ORDER BY RAND() LIMIT 1');
$stmt->bind_param("i", $mapId);
$stmt->execute();
$place = $stmt->get_result()->fetch_assoc();
$realPosition = new Position($place['lat'], $place['lng']);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$data = compact('bounds');
return new HtmlView('game', $data);
}
}

View File

@ -0,0 +1,28 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Util\Geo\Position;
use MapGuesser\View\JsonView;
use MapGuesser\View\ViewBase;
use mysqli;
class GetNewPosition implements ControllerInterface
{
public function run(): ViewBase
{
$mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
// demo map
$mapId = 1;
// using RAND() for the time being, could be changed in the future
$stmt = $mysql->prepare('SELECT lat, lng FROM places WHERE map_id=? ORDER BY RAND() LIMIT 1');
$stmt->bind_param("i", $mapId);
$stmt->execute();
$place = $stmt->get_result()->fetch_assoc();
$position = new Position($place['lat'], $place['lng']);
$data = ['position' => $position->toArray()];
return new JsonView($data);
}
}

View File

@ -1,31 +0,0 @@
<?php namespace MapGuesser\Geo;
class Position
{
private float $lat;
private float $lng;
public function __construct(float $lat, float $lng)
{
$this->lat = $lat;
$this->lng = $lng;
}
public function getLat(): float
{
return $this->lat;
}
public function getLng(): float
{
return $this->lng;
}
public function toJson(): string
{
return json_encode([
'lat' => $this->lat,
'lng' => $this->lng,
]);
}
}

View File

@ -1,7 +1,9 @@
<?php namespace MapGuesser\Geo; <?php namespace MapGuesser\Util\Geo;
class Bounds class Bounds
{ {
const ONE_DEGREE_OF_LATITUDE_IN_METER = 111132.954;
private float $southLat; private float $southLat;
private float $westLng; private float $westLng;
@ -10,13 +12,27 @@ class Bounds
private bool $initialized = false; private bool $initialized = false;
public function __construct(Position $position = null) public static function createWithPosition(Position $position): Bounds
{ {
if ($position === null) { $instance = new static();
return;
$instance->initialize($position);
return $instance;
} }
$this->initialize($position); public static function createDirectly(float $southLat, float $westLng, float $northLat, float $eastLng): Bounds
{
$instance = new static();
$instance->southLat = $southLat;
$instance->westLng = $westLng;
$instance->northLat = $northLat;
$instance->eastLng = $eastLng;
$instance->initialized = true;
return $instance;
} }
public function extend(Position $position): void public function extend(Position $position): void
@ -47,6 +63,18 @@ class Bounds
} }
} }
public function calculateApproximateArea(): float
{
$dLat = $this->northLat - $this->southLat;
$dLng = $this->eastLng - $this->westLng;
$m = $dLat * static::ONE_DEGREE_OF_LATITUDE_IN_METER;
$a = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_METER * cos(deg2rad($this->northLat));
$c = $dLng * static::ONE_DEGREE_OF_LATITUDE_IN_METER * cos(deg2rad($this->southLat));
return $m * ($a + $c) / 2;
}
public function toJson(): string public function toJson(): string
{ {
if (!$this->initialized) { if (!$this->initialized) {

56
src/Util/Geo/Position.php Normal file
View File

@ -0,0 +1,56 @@
<?php namespace MapGuesser\Util\Geo;
class Position
{
const EARTH_RADIUS_IN_METER = 6371000;
private float $lat;
private float $lng;
public function __construct(float $lat, float $lng)
{
$this->lat = $lat;
$this->lng = $lng;
}
public function getLat(): float
{
return $this->lat;
}
public function getLng(): float
{
return $this->lng;
}
public function calculateDistanceTo(Position $otherPosition): float
{
$lat1 = deg2rad($this->lat);
$lng1 = deg2rad($this->lng);
$lat2 = deg2rad($otherPosition->lat);
$lng2 = deg2rad($otherPosition->lng);
$angleCos = cos($lat1) * cos($lat2) * cos($lng2 - $lng1) + sin($lat1) * sin($lat2);
if ($angleCos > 1.0) {
$angleCos = 1.0;
}
$angle = acos($angleCos);
return $angle * static::EARTH_RADIUS_IN_METER;
}
public function toArray(): array
{
return [
'lat' => $this->lat,
'lng' => $this->lng,
];
}
public function toJson(): string
{
return json_encode($this->toArray());
}
}

29
src/View/HtmlView.php Normal file
View File

@ -0,0 +1,29 @@
<?php namespace MapGuesser\View;
class HtmlView extends ViewBase
{
private string $template;
public function __construct(string $template, array &$data = [])
{
$this->template = $template;
$this->data = &$data;
}
public function &render(): string
{
extract($this->data);
ob_start();
require ROOT . '/views/' . $this->template . '.php';
$content = ob_get_contents();
ob_end_clean();
return $content;
}
public function getContentType(): string
{
return 'text/html';
}
}

21
src/View/JsonView.php Normal file
View File

@ -0,0 +1,21 @@
<?php namespace MapGuesser\View;
class JsonView extends ViewBase
{
public function __construct(array &$data = [])
{
$this->data = &$data;
}
public function &render(): string
{
$content = json_encode($this->data);
return $content;
}
public function getContentType(): string
{
return 'application/json';
}
}

15
src/View/ViewBase.php Normal file
View File

@ -0,0 +1,15 @@
<?php namespace MapGuesser\View;
abstract class ViewBase
{
protected array $data;
public function &getData(): array
{
return $this->data;
}
abstract public function &render(): string;
abstract public function getContentType(): string;
}

44
views/game.php Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MapGuesser</title>
<link href="static/css/mapguesser.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&display=swap" rel="stylesheet">
</head>
<body>
<div id="loading">
<img src="static/img/loading.gif">
</div>
<div id="panorama"></div>
<div id="guess">
<div id="guessMap"></div>
<div id="guessButtonContainer">
<button id="guessButton" disabled>Guess</button>
</div>
</div>
<div id="result">
<div id="resultMap"></div>
<div id="resultInfo">
<div>
<p>You were <span id="distance" class="bold"></span> close.</p>
</div>
<div>
<p>You earned <span id="score" class="bold"></span> points.</p>
</div>
<div>
<div id="scoreBarBase"><div id="scoreBar"></div></div>
</div>
<div>
<button id="continueButton">Continue</button>
</div>
</div>
</div>
<script>
var mapArea = <?= $bounds->calculateApproximateArea() ?>;
var guessMapBounds = <?= $bounds->toJson() ?>;
</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>
</body>
</html>