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 + + + + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+

You were close.

+
+
+

You earned points.

+
+
+
+
+
+ +
+
+
+ + + + +