Merged in develop (pull request #12)
Develop
This commit is contained in:
commit
d1b0522fdd
@ -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
|
||||
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
.env
|
||||
installed
|
||||
vendor
|
||||
|
27
db/mapguesser.sql
Normal file
27
db/mapguesser.sql
Normal 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;
|
@ -13,7 +13,6 @@ services:
|
||||
volumes:
|
||||
- mysql:/var/lib/mysql
|
||||
environment:
|
||||
#TZ: Europe/Budapest
|
||||
MYSQL_ROOT_PASSWORD: 'root'
|
||||
MYSQL_DATABASE: 'mapguesser'
|
||||
MYSQL_USER: 'mapguesser'
|
||||
|
@ -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
|
||||
|
@ -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');")"
|
||||
|
||||
|
4
main.php
4
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'])) {
|
||||
|
5
public/.htaccess
Normal file
5
public/.htaccess
Normal file
@ -0,0 +1,5 @@
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
@ -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');
|
||||
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
echo $view->render();
|
||||
|
@ -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;
|
||||
}
|
||||
|
BIN
public/static/img/loading.gif
(Stored with Git LFS)
Normal file
BIN
public/static/img/loading.gif
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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();
|
||||
}
|
||||
})();
|
||||
|
23
scripts/install.sh
Executable file
23
scripts/install.sh
Executable 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
12
scripts/update.sh
Executable 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
|
8
src/Controller/ControllerInterface.php
Normal file
8
src/Controller/ControllerInterface.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use MapGuesser\View\ViewBase;
|
||||
|
||||
interface ControllerInterface
|
||||
{
|
||||
public function run(): ViewBase;
|
||||
}
|
35
src/Controller/GameController.php
Normal file
35
src/Controller/GameController.php
Normal 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);
|
||||
}
|
||||
}
|
28
src/Controller/GetNewPosition.php
Normal file
28
src/Controller/GetNewPosition.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
<?php namespace MapGuesser\Geo;
|
||||
<?php namespace MapGuesser\Util\Geo;
|
||||
|
||||
class Bounds
|
||||
{
|
||||
const ONE_DEGREE_OF_LATITUDE_IN_METER = 111132.954;
|
||||
|
||||
private float $southLat;
|
||||
private float $westLng;
|
||||
|
||||
@ -10,13 +12,27 @@ class Bounds
|
||||
|
||||
private bool $initialized = false;
|
||||
|
||||
public function __construct(Position $position = null)
|
||||
public static function createWithPosition(Position $position): Bounds
|
||||
{
|
||||
if ($position === null) {
|
||||
return;
|
||||
}
|
||||
$instance = new static();
|
||||
|
||||
$this->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) {
|
56
src/Util/Geo/Position.php
Normal file
56
src/Util/Geo/Position.php
Normal 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
29
src/View/HtmlView.php
Normal 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
21
src/View/JsonView.php
Normal 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
15
src/View/ViewBase.php
Normal 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
44
views/game.php
Normal 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>
|
Loading…
Reference in New Issue
Block a user