diff --git a/.env.example b/.env.example
index 097e309..6d5a0de 100644
--- a/.env.example
+++ b/.env.example
@@ -3,4 +3,5 @@ DB_HOST=mariadb
DB_USER=mapguesser
DB_PASSWORD=mapguesser
DB_NAME=mapguesser
+GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
diff --git a/composer.json b/composer.json
index 8010c24..6325f0a 100644
--- a/composer.json
+++ b/composer.json
@@ -4,7 +4,8 @@
"description": "MapGuesser Application",
"license": "GNU GPL 3.0",
"require": {
- "vlucas/phpdotenv": "^4.1"
+ "vlucas/phpdotenv": "^4.1",
+ "romanpitak/php-rest-client": "^1.2"
},
"require-dev": {},
"autoload": {
@@ -17,4 +18,4 @@
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]
}
-}
\ No newline at end of file
+}
diff --git a/composer.lock b/composer.lock
index 4bbcd4f..fc6069d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "29431cf83ee884f01ee954b1068c0ccc",
+ "content-hash": "3778b9431ef3d22705bdbf1653102a1a",
"packages": [
{
"name": "phpoption/phpoption",
@@ -61,6 +61,51 @@
],
"time": "2020-03-21T18:07:53+00:00"
},
+ {
+ "name": "romanpitak/php-rest-client",
+ "version": "v1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/romanpitak/PHP-REST-Client.git",
+ "reference": "728b6c44040a13daeb8033953f5f3cdd3dde1980"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/romanpitak/PHP-REST-Client/zipball/728b6c44040a13daeb8033953f5f3cdd3dde1980",
+ "reference": "728b6c44040a13daeb8033953f5f3cdd3dde1980",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "RestClient\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Roman Piták",
+ "email": "roman@pitak.net",
+ "homepage": "http://pitak.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "REST client library.",
+ "homepage": "https://github.com/romanpitak/PHP-REST-Client",
+ "keywords": [
+ "client",
+ "http",
+ "rest"
+ ],
+ "time": "2015-02-19T15:32:16+00:00"
+ },
{
"name": "symfony/polyfill-ctype",
"version": "v1.17.0",
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 9e02cc7..f75e34b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -5,7 +5,7 @@ ENV DEBIAN_FRONTEND noninteractive
# Install Apache, PHP and further necessary packages
RUN apt update
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-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
# Configure Apache with PHP
RUN mkdir -p /run/php
diff --git a/main.php b/main.php
index b8c37f0..b2468f9 100644
--- a/main.php
+++ b/main.php
@@ -14,3 +14,5 @@ if (!empty($_ENV['DEV'])) {
} else {
ini_set('display_errors', '0');
}
+
+session_start();
diff --git a/public/index.php b/public/index.php
index ee4c162..a20f4d7 100644
--- a/public/index.php
+++ b/public/index.php
@@ -5,12 +5,15 @@ require '../main.php';
// very basic routing
$host = $_SERVER["REQUEST_SCHEME"] . '://' . $_SERVER["SERVER_NAME"];
$url = $_SERVER['REQUEST_URI'];
+if (($pos = strpos($url, '?')) !== false) {
+ $url = substr($url, 0, $pos);
+}
switch($url) {
case '/game':
$controller = new MapGuesser\Controller\GameController();
break;
- case '/getNewPosition.json':
- $controller = new MapGuesser\Controller\GetNewPosition();
+ case '/position.json':
+ $controller = new MapGuesser\Controller\PositionController();
break;
case '/':
header('Location: ' . $host . '/game', true, 302);
diff --git a/public/static/js/mapguesser.js b/public/static/js/mapguesser.js
index 4ff40ae..8dcee39 100644
--- a/public/static/js/mapguesser.js
+++ b/public/static/js/mapguesser.js
@@ -7,7 +7,7 @@
rounds: [],
scoreSum: 0,
- realPosition: null,
+ panoId: null,
panorama: null,
map: null,
guessMarker: null,
@@ -15,32 +15,24 @@
googleLink: null,
initialize: function () {
- if (sessionStorage.rounds) {
- var roundsLoaded = JSON.parse(sessionStorage.rounds);
- for (var i = 0; i < roundsLoaded.length; ++i) {
- var round = roundsLoaded[i];
- Core.rounds.push({ realPosition: round.realPosition, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
- if (round.guessPosition) {
- Core.addRealGuessPair(round.realPosition, round.guessPosition, true);
+ document.getElementById('currentRound').innerHTML = '1/' + String(Core.NUMBER_OF_ROUNDS);
+ document.getElementById('currentScoreSum').innerHTML = '0/0';
+
+ Core.getNewPosition(function (response) {
+ if (response.history) {
+ for (var i = 0; i < response.history.length; ++i) {
+ var round = response.history[i];
+ Core.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
+ Core.addRealGuessPair(round.position, round.guessPosition, true);
+ Core.scoreSum += round.score;
}
+
+ document.getElementById('currentRound').innerHTML = String(Core.rounds.length) + '/' + String(Core.NUMBER_OF_ROUNDS);
+ document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_SCORE);
}
- }
- if (sessionStorage.scoreSum) {
- Core.scoreSum = parseInt(sessionStorage.scoreSum);
- }
-
- if (Core.rounds.length > 0 && !Core.rounds[Core.rounds.length - 1].guessPosition) {
- Core.realPosition = Core.rounds[Core.rounds.length - 1].realPosition;
- Core.loadPositionInfo(Core.realPosition);
-
- document.getElementById('currentRound').innerHTML = String(Core.rounds.length) + '/' + String(Core.NUMBER_OF_ROUNDS);
- document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String((Core.rounds.length - 1) * Core.MAX_SCORE);
- } else {
Core.startNewRound();
-
- document.getElementById('currentScoreSum').innerHTML = String(0);
- }
+ });
},
startNewGame: function () {
@@ -64,9 +56,13 @@
document.getElementById('continueButton').style.display = null;
document.getElementById('startNewGameButton').style.display = null;
+ document.getElementById('currentScoreSum').innerHTML = '0/0';
+
Core.prepareNewRound();
- document.getElementById('currentScoreSum').innerHTML = String(0);
+ Core.getNewPosition(function () {
+ Core.startNewRound();
+ });
},
prepareNewRound: function () {
@@ -87,113 +83,107 @@
Core.map.setOptions({
draggableCursor: 'crosshair'
});
- Core.map.fitBounds(guessMapBounds);
-
- Core.startNewRound();
+ Core.map.fitBounds(mapBounds);
},
startNewRound: function () {
- Core.rounds.push({ realPosition: null, guessPosition: null, realMarker: null, guessMarker: null, line: null });
-
- Core.panorama.setVisible(false);
- document.getElementById('loading').style.visibility = 'visible';
+ Core.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);
- Core.getNewPosition();
+ Core.loadPano(Core.panoId);
},
- getNewPosition: function () {
+ getNewPosition: function (callback) {
+ document.getElementById('loading').style.visibility = 'visible';
+
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
- Core.realPosition = this.response.position;
- Core.rounds[Core.rounds.length - 1].realPosition = this.response.position;
- Core.loadPositionInfo(this.response.position);
+ document.getElementById('loading').style.visibility = 'hidden';
- Core.saveToSession();
+ Core.panoId = this.response.panoId;
+
+ callback(this.response);
}
};
- xhr.open('GET', 'getNewPosition.json', true);
+ xhr.open('GET', 'position.json', true);
xhr.send();
},
- loadPositionInfo: function (position) {
- var sv = new google.maps.StreetViewService();
- sv.getPanorama({ location: position, preference: google.maps.StreetViewPreference.NEAREST, source: google.maps.StreetViewSource.OUTDOOR }, Core.loadPano);
- },
-
- loadPano: function (data, status) {
- if (status !== google.maps.StreetViewStatus.OK) {
- Core.getNewPosition();
- return;
- }
-
- document.getElementById('loading').style.visibility = 'hidden';
-
+ loadPano: function (panoId) {
if (Core.adaptGuess) {
document.getElementById('guess').classList.add('adapt');
}
- Core.panorama.setVisible(true);
Core.panorama.setPov({ heading: 0, pitch: 0 });
Core.panorama.setZoom(0);
- Core.panorama.setPano(data.location.pano);
+ Core.panorama.setPano(panoId);
},
evaluateGuess: function () {
var guessPosition = Core.guessMarker.getPosition().toJSON();
Core.rounds[Core.rounds.length - 1].guessPosition = guessPosition;
- var distance = Util.calculateDistance(Core.realPosition, guessPosition);
- var score = Core.calculateScore(distance);
- Core.scoreSum += score;
-
- document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_SCORE);
-
- Core.saveToSession();
-
- Core.guessMarker.setMap(null);
- Core.guessMarker = null;
-
if (Core.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
- document.getElementById('guess').classList.add('result');
+ document.getElementById('loading').style.visibility = 'visible';
- Core.addRealGuessPair(Core.realPosition, guessPosition);
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = 'json';
+ xhr.onreadystatechange = function () {
+ if (this.readyState == 4 && this.status == 200) {
+ Core.guessMarker.setMap(null);
+ Core.guessMarker = null;
- var resultBounds = new google.maps.LatLngBounds();
- resultBounds.extend(Core.realPosition);
- resultBounds.extend(guessPosition);
+ document.getElementById('loading').style.visibility = 'hidden';
+ document.getElementById('guess').classList.add('result');
- Core.map.setOptions({
- draggableCursor: 'grab'
- });
- Core.map.fitBounds(resultBounds);
+ Core.scoreSum += this.response.result.score;
+ document.getElementById('currentScoreSum').innerHTML = String(Core.scoreSum) + '/' + String(Core.rounds.length * Core.MAX_SCORE);
- document.getElementById('distance').innerHTML = Util.printDistanceForHuman(distance);
- document.getElementById('score').innerHTML = score;
+ Core.rounds[Core.rounds.length - 1].position = this.response.result.position;
+ Core.addRealGuessPair(this.response.result.position, guessPosition);
- var scoreBarProperties = Core.calculateScoreBarProperties(score, Core.MAX_SCORE);
- var scoreBar = document.getElementById('scoreBar');
- scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
- scoreBar.style.width = scoreBarProperties.width;
+ var resultBounds = new google.maps.LatLngBounds();
+ resultBounds.extend(this.response.result.position);
+ resultBounds.extend(guessPosition);
- if (Core.rounds.length == Core.NUMBER_OF_ROUNDS) {
- document.getElementById('continueButton').style.display = 'none';
- document.getElementById('showSummaryButton').style.display = 'block';
- }
+ Core.map.setOptions({
+ draggableCursor: 'grab'
+ });
+ Core.map.fitBounds(resultBounds);
+
+ document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
+ document.getElementById('score').innerHTML = this.response.result.score;
+
+ var scoreBarProperties = Core.calculateScoreBarProperties(this.response.result.score, Core.MAX_SCORE);
+ var scoreBar = document.getElementById('scoreBar');
+ scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
+ scoreBar.style.width = scoreBarProperties.width;
+
+ if (Core.rounds.length == Core.NUMBER_OF_ROUNDS) {
+ document.getElementById('continueButton').style.display = 'none';
+ document.getElementById('showSummaryButton').style.display = 'block';
+ }
+
+ Core.panoId = this.response.panoId;
+ }
+ };
+ xhr.open('POST', 'position.json', true);
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.send('guess&lat=' + guessPosition.lat + '&lng=' + guessPosition.lng);
},
- addRealGuessPair: function (realPosition, guessPosition, hidden) {
+ addRealGuessPair: function (position, guessPosition, hidden) {
var round = Core.rounds[Core.rounds.length - 1];
round.realMarker = new google.maps.Marker({
map: Core.map,
visible: !hidden,
- position: realPosition,
+ position: position,
title: 'Open in Google Maps',
zIndex: Core.rounds.length * 2,
clickable: true,
@@ -224,7 +214,7 @@
map: Core.map,
visible: !hidden,
path: [
- realPosition,
+ position,
guessPosition
],
geodesic: true,
@@ -245,12 +235,6 @@
});
},
- calculateScore: function (distance) {
- var goodness = 1.0 - distance / Math.sqrt(mapArea);
-
- return Math.round(Math.pow(Core.MAX_SCORE, goodness));
- },
-
calculateScoreBarProperties: function (score, maxScore) {
var percent = Math.floor((score / maxScore) * 100);
@@ -292,7 +276,7 @@
round.guessMarker.setVisible(true);
round.line.setVisible(true);
- resultBounds.extend(round.realPosition);
+ resultBounds.extend(round.position);
resultBounds.extend(round.guessPosition);
}
@@ -324,51 +308,10 @@
Core.googleLink.href = 'https://maps.google.com/maps';
}
}, 1);
- },
-
- saveToSession: function () {
- if (Core.rounds.length == Core.NUMBER_OF_ROUNDS && Core.rounds[Core.rounds.length - 1].guessPosition) {
- sessionStorage.removeItem('rounds');
- sessionStorage.removeItem('scoreSum');
- return;
- }
-
- var roundsToSave = [];
- for (var i = 0; i < Core.rounds.length; ++i) {
- var round = Core.rounds[i];
- roundsToSave.push({ realPosition: round.realPosition, guessPosition: round.guessPosition });
- }
-
- sessionStorage.setItem('rounds', JSON.stringify(roundsToSave));
- sessionStorage.setItem('scoreSum', String(Core.scoreSum));
}
};
var Util = {
- EARTH_RADIUS_IN_METER: 6371000,
-
- deg2rad: function (deg) {
- return deg * (Math.PI / 180.0);
- },
-
- calculateDistance: function (position1, position2) {
- var lat1 = Util.deg2rad(position1.lat);
- var lng1 = Util.deg2rad(position1.lng);
- var lat2 = Util.deg2rad(position2.lat);
- var lng2 = Util.deg2rad(position2.lng);
-
- var angleCos = Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1) +
- Math.sin(lat1) * Math.sin(lat2);
-
- 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';
@@ -393,7 +336,7 @@
draggingCursor: 'grabbing'
});
- Core.map.fitBounds(guessMapBounds);
+ Core.map.fitBounds(mapBounds);
Core.map.addListener('click', function (e) {
if (Core.rounds[Core.rounds.length - 1].guessPosition) {
@@ -461,6 +404,7 @@
document.getElementById('continueButton').onclick = function () {
Core.prepareNewRound();
+ Core.startNewRound();
}
document.getElementById('showSummaryButton').onclick = function () {
diff --git a/scripts/update.sh b/scripts/update.sh
index 46e0cd4..d54aab2 100755
--- a/scripts/update.sh
+++ b/scripts/update.sh
@@ -4,6 +4,9 @@ ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
+echo "Installing Composer packages..."
+(cd ${ROOT_DIR} && composer install)
+
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Minifying JS, CSS and SVG files..."
diff --git a/src/Controller/GameController.php b/src/Controller/GameController.php
index 4e74cd0..43b04e0 100644
--- a/src/Controller/GameController.php
+++ b/src/Controller/GameController.php
@@ -1,28 +1,47 @@
mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
+ }
+
public function run(): ViewBase
{
- $mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
+ $bounds = $this->getMapBounds();
- // demo map
- $mapId = 1;
+ if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $this->mapId) {
+ $_SESSION['state'] = [
+ 'mapId' => $this->mapId,
+ 'area' => $bounds->calculateApproximateArea(),
+ 'rounds' => []
+ ];
+ }
- $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);
+ $data = ['bounds' => $bounds->toArray()];
+ return new HtmlView('game', $data);
+ }
+
+ private function getMapBounds(): Bounds
+ {
+ $stmt = $this->mysql->prepare('SELECT bound_south_lat, bound_west_lng, bound_north_lat, bound_east_lng FROM maps WHERE id=?');
+ $stmt->bind_param("i", $this->mapId);
$stmt->execute();
$map = $stmt->get_result()->fetch_assoc();
$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);
+ return $bounds;
}
}
diff --git a/src/Controller/GetNewPosition.php b/src/Controller/GetNewPosition.php
deleted file mode 100644
index f0abf75..0000000
--- a/src/Controller/GetNewPosition.php
+++ /dev/null
@@ -1,34 +0,0 @@
-prepare('SELECT COUNT(*) AS num FROM places WHERE map_id=?');
- $stmt->bind_param("i", $mapId);
- $stmt->execute();
- $numberOfPlaces = $stmt->get_result()->fetch_assoc()['num'];
-
- $randomOffset = random_int(0, $numberOfPlaces-1);
-
- $stmt = $mysql->prepare('SELECT lat, lng FROM places WHERE map_id=? ORDER BY id LIMIT 1 OFFSET ?');
- $stmt->bind_param("ii", $mapId, $randomOffset);
- $stmt->execute();
- $place = $stmt->get_result()->fetch_assoc();
-
- $position = new Position($place['lat'], $place['lng']);
-
- $data = ['position' => $position->toArray()];
- return new JsonView($data);
- }
-}
diff --git a/src/Controller/PositionController.php b/src/Controller/PositionController.php
new file mode 100644
index 0000000..900b1c0
--- /dev/null
+++ b/src/Controller/PositionController.php
@@ -0,0 +1,180 @@
+mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
+ }
+
+ public function run(): ViewBase
+ {
+ if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $this->mapId) {
+ return new JsonView(['error' => 'No valid session found!']);
+ }
+
+ if (count($_SESSION['state']['rounds']) === 0) {
+ $newPosition = $this->getNewPosition();
+ $_SESSION['state']['rounds'][] = $newPosition;
+
+ $data = ['panoId' => $newPosition['panoId']];
+ } elseif (isset($_POST['guess'])) {
+ $last = &$_SESSION['state']['rounds'][count($_SESSION['state']['rounds']) - 1];
+
+ $position = $last['position'];
+ $guessPosition = new Position((float) $_POST['lat'], (float) $_POST['lng']);
+
+ $last['guessPosition'] = $guessPosition;
+
+ $distance = $this->calculateDistance($position, $guessPosition);
+ $score = $this->calculateScore($distance, $_SESSION['state']['area']);
+
+ $last['distance'] = $distance;
+ $last['score'] = $score;
+
+ if (count($_SESSION['state']['rounds']) < static::NUMBER_OF_ROUNDS) {
+ $exclude = [];
+
+ foreach ($_SESSION['state']['rounds'] as $round) {
+ $exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
+ }
+
+ $newPosition = $this->getNewPosition($exclude);
+ $_SESSION['state']['rounds'][] = $newPosition;
+
+ $panoId = $newPosition['panoId'];
+ } else {
+ $_SESSION['state']['rounds'] = [];
+
+ $panoId = null;
+ }
+
+ $data = [
+ 'result' => [
+ 'position' => $position->toArray(),
+ 'distance' => $distance,
+ 'score' => $score
+ ],
+ 'panoId' => $panoId
+ ];
+ } else {
+ $rounds = count($_SESSION['state']['rounds']);
+ $last = $_SESSION['state']['rounds'][$rounds - 1];
+
+ $history = [];
+ for ($i = 0; $i < $rounds - 1; ++$i) {
+ $round = $_SESSION['state']['rounds'][$i];
+ $history[] = [
+ 'position' => $round['position']->toArray(),
+ 'guessPosition' => $round['guessPosition']->toArray(),
+ 'distance' => $round['distance'],
+ 'score' => $round['score']
+ ];
+ }
+
+ $data = [
+ 'history' => $history,
+ 'panoId' => $last['panoId']
+ ];
+ }
+
+ return new JsonView($data);
+ }
+
+ private function getNewPosition(array $exclude = []): array
+ {
+ $placesWithoutPano = [];
+
+ do {
+ $place = $this->selectNewPlace($exclude);
+ $position = new Position($place['lat'], $place['lng']);
+ $panoId = $this->getPanorama($position);
+
+ if ($panoId === null) {
+ $placesWithoutPano[] = $place['id'];
+ }
+ } while ($panoId === null);
+
+ return [
+ 'placesWithoutPano' => $placesWithoutPano,
+ 'placeId' => $place['id'],
+ 'position' => $position,
+ 'panoId' => $panoId
+ ];
+ }
+
+ private function selectNewPlace(array $exclude): array
+ {
+ $condition = '';
+ $params = ['i', &$this->mapId];
+ if (($numExcluded = count($exclude)) > 0) {
+ $condition .= ' AND id NOT IN (' . implode(',', array_fill(0, $numExcluded, '?')) . ')';
+ $params[0] .= str_repeat('i', $numExcluded);
+ foreach ($exclude as &$placeId) {
+ $params[] = &$placeId;
+ }
+ }
+
+ $stmt = $this->mysql->prepare('SELECT COUNT(*) AS num FROM places WHERE map_id=? ' . $condition . '');
+ call_user_func_array([$stmt, 'bind_param'], $params);
+ $stmt->execute();
+ $numberOfPlaces = $stmt->get_result()->fetch_assoc()['num'];
+ $randomOffset = random_int(0, $numberOfPlaces - 1);
+
+ $params[0] .= 'i';
+ $params[] = &$randomOffset;
+
+ $stmt = $this->mysql->prepare('SELECT id, lat, lng FROM places WHERE map_id=? ' . $condition . ' ORDER BY id LIMIT 1 OFFSET ?');
+ call_user_func_array([$stmt, 'bind_param'], $params);
+ $stmt->execute();
+
+ return $stmt->get_result()->fetch_assoc();
+ }
+
+ private function getPanorama(Position $position): ?string
+ {
+ $query = [
+ 'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
+ 'location' => $position->getLat() . ',' . $position->getLng(),
+ 'source' => 'outdoor'
+ ];
+
+ $client = new Client('https://maps.googleapis.com/maps/api/streetview');
+ $request = $client->newRequest('metadata?' . http_build_query($query));
+ $response = $request->getResponse();
+
+ $panoData = json_decode($response->getParsedResponse(), true);
+
+ if ($panoData['status'] !== 'OK') {
+ return null;
+ }
+
+ return $panoData['pano_id'];
+ }
+
+ private function calculateDistance(Position $realPosition, Position $guessPosition): float
+ {
+ return $realPosition->calculateDistanceTo($guessPosition);
+ }
+
+ private function calculateScore(float $distance, float $area)
+ {
+ $goodness = 1.0 - ($distance / sqrt($area));
+
+ return round(pow(static::MAX_SCORE, $goodness));
+ }
+}
diff --git a/src/Util/Geo/Bounds.php b/src/Util/Geo/Bounds.php
index c77d2b1..7b448be 100644
--- a/src/Util/Geo/Bounds.php
+++ b/src/Util/Geo/Bounds.php
@@ -75,18 +75,23 @@ class Bounds
return $m * ($a + $c) / 2;
}
- public function toJson(): string
+ public function toArray(): array
{
if (!$this->initialized) {
throw new \Exception("Bounds are not initialized!");
}
- return json_encode([
+ return [
'south' => $this->southLat,
'west' => $this->westLng,
'north' => $this->northLat,
'east' => $this->eastLng,
- ]);
+ ];
+ }
+
+ public function toJson(): string
+ {
+ return json_encode($this->toArray());
}
private function initialize(Position $position)
diff --git a/views/game.php b/views/game.php
index 07a03e0..fee7284 100644
--- a/views/game.php
+++ b/views/game.php
@@ -48,8 +48,7 @@