diff --git a/.env.example b/.env.example
index 1570bcb..097e309 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fe357d6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.gif filter=lfs diff=lfs merge=lfs -text
+
diff --git a/.gitignore b/.gitignore
index 226ca36..9bdf82a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.env
+installed
vendor
diff --git a/db/mapguesser.sql b/db/mapguesser.sql
new file mode 100644
index 0000000..538e3da
--- /dev/null
+++ b/db/mapguesser.sql
@@ -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;
diff --git a/docker-compose.yml b/docker-compose.yml
index e00f9aa..3effcfe 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,7 +13,6 @@ services:
volumes:
- mysql:/var/lib/mysql
environment:
- #TZ: Europe/Budapest
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser'
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 16f7048..1d55371 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -4,13 +4,9 @@ ENV DEBIAN_FRONTEND noninteractive
# Install Apache, PHP and further necessary packages
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
-# Configure tzdata
-#RUN ln -fs /usr/share/zoneinfo/Europe/Budapest /etc/localtime
-#RUN dpkg-reconfigure --frontend noninteractive tzdata
-
# Configure Apache with PHP
RUN mkdir -p /run/php
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
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
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
diff --git a/docker/scripts/install-composer.sh b/docker/scripts/install-composer.sh
index f55d62e..65dbc88 100755
--- a/docker/scripts/install-composer.sh
+++ b/docker/scripts/install-composer.sh
@@ -1,6 +1,6 @@
#!/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');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
diff --git a/main.php b/main.php
index bd37059..b8c37f0 100644
--- a/main.php
+++ b/main.php
@@ -2,7 +2,9 @@
require 'vendor/autoload.php';
-$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
+const ROOT = __DIR__;
+
+$dotenv = Dotenv\Dotenv::createImmutable(ROOT);
$dotenv->load();
if (!empty($_ENV['DEV'])) {
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..3a37342
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,5 @@
+RewriteEngine On
+RewriteBase /
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^ index.php [L]
diff --git a/public/index.php b/public/index.php
index 2bb1e39..ee4c162 100644
--- a/public/index.php
+++ b/public/index.php
@@ -2,39 +2,26 @@
require '../main.php';
-// demo position
-$realPosition = new MapGuesser\Geo\Position(47.85239, 13.35101);
+// very basic routing
+$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
-$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));
+$view = $controller->run();
-?>
+header('Content-Type: ' . $view->getContentType() . '; charset=UTF-8');
-
-
-
-
-
- MapGuesser
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+echo $view->render();
diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css
index 71e354e..d22455b 100644
--- a/public/static/css/mapguesser.css
+++ b/public/static/css/mapguesser.css
@@ -1,9 +1,65 @@
+* {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ vertical-align: baseline;
+}
+
html, body {
height: 100%;
margin: 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 {
height: 100%;
width: 100%;
@@ -18,6 +74,7 @@ html, body {
height: 150px;
opacity: 0.5;
z-index: 2;
+ visibility: visible;
transition-property: width, height, opacity;
transition-duration: 0.1s;
transition-delay: 0.8s;
@@ -26,11 +83,11 @@ html, body {
#guess:hover {
width: 500px;
height: 350px;
- opacity: 1.0;
+ opacity: 0.95;
transition-delay: 0s;
}
-#guess #guessMap {
+#guess > #guessMap {
height: 115px;
width: 100%;
transition-property: height;
@@ -39,7 +96,7 @@ html, body {
border-radius: 3px;
}
-#guess:hover #guessMap {
+#guess:hover > #guessMap {
height: 315px;
transition-delay: 0s;
}
@@ -50,26 +107,61 @@ html, body {
}
#guessButton {
- cursor: pointer;
- font-size: 14px;
- font-weight: bold;
- color: #ffffff;
- background-color: #5e77aa;
- border: none;
- border-radius: 3px;
+ padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
-#guessButton:hover, #guessButton:focus {
- background-color: #29457f;
- outline: none;
+#result {
+ position: absolute;
+ top: 50px;
+ left: 50px;
+ right: 50px;
+ bottom: 50px;
+ opacity: 0.95;
+ z-index: 2;
+ visibility: hidden;
+ background-color: #ffffff;
+ border-radius: 3px;
}
-#guessButton:disabled {
- cursor: no-drop;
- color: #dddddd;
- background-color: #808080;
- opacity: 0.7;
+#resultMap {
+ height: 70%;
+ width: 100%;
+}
+
+#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;
}
diff --git a/public/static/img/loading.gif b/public/static/img/loading.gif
new file mode 100644
index 0000000..6752946
--- /dev/null
+++ b/public/static/img/loading.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4400fe604ac37f8a9f7ce77f645e89645166347976f8233a0852c2ccb0a24f0
+size 27593
diff --git a/public/static/js/mapguesser.js b/public/static/js/mapguesser.js
index bf76f05..a405aee 100644
--- a/public/static/js/mapguesser.js
+++ b/public/static/js/mapguesser.js
@@ -1,108 +1,248 @@
-Math.deg2rad = function (deg) {
- return deg * (this.PI / 180.0);
-};
+(function () {
+ var Core = {
+ MAX_SCORE: 1000,
-var Util = {
- EARTH_RADIUS_IN_METER: 6371000,
+ realPosition: null,
+ panorama: null,
+ guessMap: null,
+ guessMarker: null,
+ resultMap: null,
+ resultMarkers: { guess: null, real: null },
+ googleLink: null,
- calculateDistance: function (position1, position2) {
- var lat1 = Math.deg2rad(position1.lat);
- var lng1 = Math.deg2rad(position1.lng);
- var lat2 = Math.deg2rad(position2.lat);
- var lng2 = Math.deg2rad(position2.lng);
+ getNewPosition: function () {
+ Core.panorama.setVisible(false);
- var latDelta = lat2 - lat1;
- var lonDelta = lng2 - lng1;
+ document.getElementById('loading').style.visibility = 'visible';
- var angle = 2 * Math.asin(
- Math.sqrt(
- Math.pow(Math.sin(latDelta / 2), 2) +
- Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(lonDelta / 2), 2)
- )
- );
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = 'json';
+ xhr.onreadystatechange = function () {
+ 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();
+ },
-var MapManipulator = {
- rewriteGoogleLink: function () {
- if (!googleLink) {
- var anchors = document.getElementById('panorama').getElementsByTagName('a');
- for (var i = 0; i < anchors.length; i++) {
- var a = anchors[i];
- if (a.href.indexOf('maps.google.com/maps') !== -1) {
- googleLink = a;
- break;
+ 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 };
+ },
+
+ rewriteGoogleLink: function () {
+ if (!Core.googleLink) {
+ var anchors = document.getElementById('panorama').getElementsByTagName('a');
+ for (var i = 0; i < anchors.length; i++) {
+ var a = anchors[i];
+ if (a.href.indexOf('maps.google.com/maps') !== -1) {
+ Core.googleLink = a;
+ break;
+ }
}
}
+
+ setTimeout(function () {
+ Core.googleLink.title = 'Google Maps'
+ Core.googleLink.href = 'https://maps.google.com/maps'
+ }, 1);
}
+ };
- setTimeout(function () {
- googleLink.title = 'Google Maps'
- googleLink.href = 'https://maps.google.com/maps'
- }, 1);
- }
-};
+ var Util = {
+ EARTH_RADIUS_IN_METER: 6371000,
-var panorama;
-var guessMap;
-var guessMarker;
-var googleLink;
-
-function initialize() {
- panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
- position: realPosition,
- pov: {
- heading: 34,
- pitch: 10
+ deg2rad: function (deg) {
+ return deg * (Math.PI / 180.0);
},
- disableDefaultUI: true,
- linksControl: true,
- showRoadLabels: false
- });
- panorama.addListener('position_changed', function () {
- MapManipulator.rewriteGoogleLink();
- });
+ 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);
- panorama.addListener('pov_changed', function () {
- MapManipulator.rewriteGoogleLink();
- });
+ var angleCos = Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1) +
+ 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,
clickableIcons: false,
draggableCursor: 'crosshair'
});
- guessMap.fitBounds(guessMapBounds);
+ Core.guessMap.fitBounds(guessMapBounds);
- guessMap.addListener('click', function (e) {
- if (guessMarker) {
- guessMarker.setPosition(e.latLng);
+ Core.guessMap.addListener('click', function (e) {
+ if (Core.guessMarker) {
+ Core.guessMarker.setPosition(e.latLng);
return;
}
- guessMarker = new google.maps.Marker({
- map: guessMap,
+ Core.guessMarker = new google.maps.Marker({
+ map: Core.guessMap,
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').onclick = function () {
- if (!guessMarker) {
- return;
+ 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 () {
+ if (!Core.guessMarker) {
+ return;
+ }
+
+ var guessedPosition = Core.guessMarker.getPosition();
+
+ this.disabled = true;
+ Core.guessMarker.setMap(null);
+ Core.guessMarker = null;
+
+ 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;
}
- var guessedPosition = guessMarker.getPosition();
- var distance = Util.calculateDistance(realPosition, { lat: guessedPosition.lat(), lng: guessedPosition.lng() });
+ document.getElementById('continueButton').onclick = function () {
+ document.getElementById('scoreBar').style.width = '0';
- alert('You were ' + distance + 'm close!');
+ Core.resultMarkers.real.setMap(null);
+ Core.resultMarkers.real = null;
+ Core.resultMarkers.guess.setMap(null);
+ Core.resultMarkers.guess = null;
- this.blur();
-}
+ document.getElementById('guess').style.visibility = 'visible';
+ document.getElementById('result').style.visibility = 'hidden';
+
+ Core.guessMap.fitBounds(guessMapBounds);
+
+ Core.getNewPosition();
+ }
+})();
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100755
index 0000000..317b5f5
--- /dev/null
+++ b/scripts/install.sh
@@ -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
diff --git a/scripts/update.sh b/scripts/update.sh
new file mode 100755
index 0000000..2c435a5
--- /dev/null
+++ b/scripts/update.sh
@@ -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
diff --git a/src/Controller/ControllerInterface.php b/src/Controller/ControllerInterface.php
new file mode 100644
index 0000000..22d2f8f
--- /dev/null
+++ b/src/Controller/ControllerInterface.php
@@ -0,0 +1,8 @@
+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);
+ }
+}
diff --git a/src/Controller/GetNewPosition.php b/src/Controller/GetNewPosition.php
new file mode 100644
index 0000000..565a3d2
--- /dev/null
+++ b/src/Controller/GetNewPosition.php
@@ -0,0 +1,28 @@
+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);
+ }
+}
diff --git a/src/Geo/Position.php b/src/Geo/Position.php
deleted file mode 100644
index c79d22b..0000000
--- a/src/Geo/Position.php
+++ /dev/null
@@ -1,31 +0,0 @@
-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,
- ]);
- }
-}
diff --git a/src/Geo/Bounds.php b/src/Util/Geo/Bounds.php
similarity index 56%
rename from src/Geo/Bounds.php
rename to src/Util/Geo/Bounds.php
index f813f0e..c77d2b1 100644
--- a/src/Geo/Bounds.php
+++ b/src/Util/Geo/Bounds.php
@@ -1,7 +1,9 @@
-initialize($position);
+ $instance->initialize($position);
+
+ return $instance;
+ }
+
+ 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
@@ -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
{
if (!$this->initialized) {
diff --git a/src/Util/Geo/Position.php b/src/Util/Geo/Position.php
new file mode 100644
index 0000000..33eb0cd
--- /dev/null
+++ b/src/Util/Geo/Position.php
@@ -0,0 +1,56 @@
+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());
+ }
+}
diff --git a/src/View/HtmlView.php b/src/View/HtmlView.php
new file mode 100644
index 0000000..35f5e9a
--- /dev/null
+++ b/src/View/HtmlView.php
@@ -0,0 +1,29 @@
+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';
+ }
+}
diff --git a/src/View/JsonView.php b/src/View/JsonView.php
new file mode 100644
index 0000000..78eb90f
--- /dev/null
+++ b/src/View/JsonView.php
@@ -0,0 +1,21 @@
+data = &$data;
+ }
+
+ public function &render(): string
+ {
+ $content = json_encode($this->data);
+
+ return $content;
+ }
+
+ public function getContentType(): string
+ {
+ return 'application/json';
+ }
+}
diff --git a/src/View/ViewBase.php b/src/View/ViewBase.php
new file mode 100644
index 0000000..9bad81f
--- /dev/null
+++ b/src/View/ViewBase.php
@@ -0,0 +1,15 @@
+data;
+ }
+
+ abstract public function &render(): string;
+
+ abstract public function getContentType(): string;
+}
diff --git a/views/game.php b/views/game.php
new file mode 100644
index 0000000..1aa5b7a
--- /dev/null
+++ b/views/game.php
@@ -0,0 +1,44 @@
+
+
+
+
+ MapGuesser
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+