Compare commits

..

No commits in common. "develop" and "Release_2005" have entirely different histories.

184 changed files with 2223 additions and 13159 deletions

View File

@ -1,5 +1,3 @@
APP_NAME=MapGuesser
APP_URL=mapguesser.dev
DEV=1 DEV=1
DB_HOST=mariadb DB_HOST=mariadb
DB_USER=mapguesser DB_USER=mapguesser
@ -7,20 +5,3 @@ DB_PASSWORD=mapguesser
DB_NAME=mapguesser DB_NAME=mapguesser
GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key GOOGLE_MAPS_SERVER_API_KEY=your_google_maps_server_api_key
GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key GOOGLE_MAPS_JS_API_KEY=your_google_maps_js_api_key
LEAFLET_TILESERVER_URL=a_leaflet_compatible_tileserver_url
LEAFLET_TILESERVER_SUBDOMAINS=list_of_subdomains_for_the_tileserver_without_separators
LEAFLET_TILESERVER_ATTRIBUTION=attribution_to_be_shown_for_tiles
STATIC_ROOT=/static
MAIL_FROM=mapguesser@mapguesser-dev.ch
MAIL_HOST=mail
MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id
MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090
ENABLE_GAME_FOR_GUESTS=0
RECAPTCHA_SITEKEY=your_recaptcha_sitekey
RECAPTCHA_SECRET=your_recaptcha_secret

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.env .env
installed installed
vendor vendor
node_modules

8
.vscode/launch.json vendored
View File

@ -9,14 +9,6 @@
"pathMappings": { "pathMappings": {
"/var/www/mapguesser": "${workspaceRoot}", "/var/www/mapguesser": "${workspaceRoot}",
} }
},
{
"name": "Listen for NodeJS Inspector in Docker",
"type": "node",
"request": "attach",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/mapguesser"
} }
] ]
} }

106
Jenkinsfile vendored
View File

@ -1,106 +0,0 @@
pipeline {
agent {
node {
label 'mapguesser'
customWorkspace 'workspace/mapguesser'
}
}
stages {
stage('Install composer') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
sh 'composer install'
}
}
stage('Unit Testing') {
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
}
post {
always {
archiveArtifacts 'unit_test_results.xml'
}
}
}
stage('Static Code Analysis') {
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
sh 'php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
}
post {
always {
archiveArtifacts 'static_code_analysis_results.json'
}
}
}
stage('Prepare Docker release') {
environment {
COMPOSER_HOME="${WORKSPACE}/.composer"
npm_config_cache="${WORKSPACE}/.npm"
}
agent {
dockerfile {
filename 'docker/Dockerfile'
dir '.'
additionalBuildArgs '--target mapg_base'
reuseNode true
}
}
steps {
script {
sh script: 'git clean -ffdx', label: 'Clean repository'
env.VERSION = sh(script: 'git describe --tags --always --match "Release_*" HEAD', returnStdout: true).trim()
sh script: 'docker/scripts/release.sh', label: 'Release script'
sh script: "rm -rf ${env.COMPOSER_HOME} ${env.npm_config_cache}"
}
}
}
stage('Release Docker image') {
steps {
script {
withDockerRegistry([credentialsId: 'gitea-system-user', url: 'https://git.esoko.eu/']) {
sh script: 'docker buildx create --use --bootstrap --platform=linux/arm64,linux/amd64 --name multi-platform-builder'
sh script: """docker buildx build \
--platform linux/amd64,linux/arm64 \
-f docker/Dockerfile \
--target mapg_release \
-t git.esoko.eu/esoko/mapguesser:${env.VERSION} \
--push \
.""",
label: 'Build Docker image'
}
}
}
}
}
}

104
README.md
View File

@ -1,105 +1,5 @@
# MapGuesser # MapGuesser
[![Build Status](https://ci.esoko.eu/job/mapguesser/job/develop/badge/icon)](https://ci.esoko.eu/job/mapguesser/job/develop/) This is the MapGuesser Application project.
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications. License: GNU AGPL 3.0
## Installation
### Set environment variables
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment. Check `.env.example` for reference.
**Important: `DEV` should NOT be set for production! See section Development if you want to use the application in development mode.**
#### API keys
**You should set the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
Required Google APIs:
* **Maps JavaScript API**: for the interactive maps and street views
* **Maps Static API**: for the static map images
* **Street View Static API**: for the backend metadata requests
Required API keys:
* **GOOGLE_MAPS_SERVER_API_KEY**: this it used by the backend and should have access to **Street View Static API**
* **GOOGLE_MAPS_JS_API_KEY**: this is used by the frontend and should have access to **Maps JavaScript API** and **Maps Static API**
Additionally, a tile provider is also needed for map editor. This should be configured by `LEAFLET_TILESERVER_URL`, `LEAFLET_TILESERVER_SUBDOMAINS` and `LEAFLET_TILESERVER_ATTRIBUTION`. You can find some providers here: https://wiki.openstreetmap.org/wiki/Tile_servers. OpenStreetMap's tile server is fine for testing.
Example:
```
LEAFLET_TILESERVER_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
LEAFLET_TILESERVER_SUBDOMAINS=abc
LEAFLET_TILESERVER_ATTRIBUTION="&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
```
### Docker Compose
Create a `docker-compose.yml` file. The example code below assumes that `.env` is placed in the same folder.
```yml
version: '3'
services:
app:
image: git.esoko.eu/esoko/mapguesser:latest
depends_on:
mariadb:
condition: service_healthy
ports:
- 80:80
- 8090:8090
volumes:
- .env:/var/www/mapguesser/.env
mariadb:
image: mariadb:10.3
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
volumes:
mysql:
```
Execute the following command:
```bash
docker compose up -d
```
**And you are done!** The application is ready to use. You can create the first administrative user with the following command after attaching to the `app` container:
```
./mapg user:add EMAIL USERNAME PASSWORD admin
```
## Development
### Set environment variables
`.env.example` should be copied to `.env` into the repo root. Only the variables for external dependencies (API keys, map attribution, etc.) should be adapted in. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value. **`DEV=1` should be set for development!**
### Docker Compose
Execute the following command from the repo root:
```bash
docker compose up -d
```
**And you are done!** You can reach the application on http://localhost. The mails that are sent by the application can be found on http://localhost:8080. If needed, the database server can be directly reached on localhost:3306, or you can use Adminer web interface on http://localhost:9090
You might have to attach to the `app` container, e.g. for creating users, `composer update`, etc.
---
*License: **GNU AGPL 3.0**. Full license text can be found in file `LICENSE`.*

View File

@ -1,26 +0,0 @@
image: php:7.4.7-cli-buster
pipelines:
default:
- step:
name: Unit Testing
caches:
- composer
artifacts:
- unit_test_results.xml
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests
- step:
name: Static Code Analysis
caches:
- composer
artifacts:
- static_code_analysis_results.json
script:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json

2
cache/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -3,19 +3,10 @@
"type": "project", "type": "project",
"description": "MapGuesser Application", "description": "MapGuesser Application",
"license": "GNU GPL 3.0", "license": "GNU GPL 3.0",
"repositories": [
{
"url": "https://git.esoko.eu/esoko/soko-web.git",
"type": "git"
}
],
"require": { "require": {
"esoko/soko-web": "0.15" "vlucas/phpdotenv": "^4.1"
},
"require-dev": {
"phpunit/phpunit": "^10.3",
"phpstan/phpstan": "^1.10"
}, },
"require-dev": {},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"MapGuesser\\": "src" "MapGuesser\\": "src"

2755
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
<?php
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Bounds;
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns(['id', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
$result = $select->execute();
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($map['id']);
$modify->set('area', $bounds->calculateApproximateArea());
$modify->save();
}

View File

@ -1,17 +0,0 @@
<?php
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(['id']);
$result = $select->execute();
while ($map = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($map['id']);
$modify->set('active', true);
$modify->save();
}

View File

@ -1,20 +0,0 @@
<?php
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Util\UsernameGenerator;
use SokoWeb\Database\Query\Select;
$select = new Select(Container::$dbConnection);
$users = Container::$persistentDataManager->selectMultipleFromDb($select, User::class);
$userRepository = new UserRepository();
$usernameGenerator = new UsernameGenerator();
foreach ($users as $user) {
do {
$username = $usernameGenerator->generate();
} while ($userRepository->getByUsername($username));
$user->setUsername($username);
Container::$persistentDataManager->saveToDb($user);
}

View File

@ -1,17 +0,0 @@
ALTER TABLE
`maps`
MODIFY
`bound_south_lat` decimal(9, 7) NOT NULL,
MODIFY
`bound_west_lng` decimal(10, 7) NOT NULL,
MODIFY
`bound_north_lat` decimal(9, 7) NOT NULL,
MODIFY
`bound_east_lng` decimal(10, 7) NOT NULL;
ALTER TABLE
`places`
MODIFY
`lat` decimal(9, 7) NOT NULL,
MODIFY
`lng` decimal(10, 7) NOT NULL;

View File

@ -1,6 +0,0 @@
ALTER TABLE
`places`
ADD
`pano_id_cached` varchar(255) NULL DEFAULT NULL,
ADD
`pano_id_cached_timestamp` timestamp NULL DEFAULT NULL;

View File

@ -1,8 +0,0 @@
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(100) NOT NULL,
`password` varchar(60) NOT NULL,
`type` enum('user', 'admin') NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,4 +0,0 @@
ALTER TABLE
`maps`
ADD
`area` decimal(13, 4) NOT NULL DEFAULT 0.0;

View File

@ -1,6 +0,0 @@
CREATE TABLE `sessions` (
`id` varchar(64) NOT NULL,
`data` text NOT NULL,
`updated` timestamp NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,14 +0,0 @@
CREATE TABLE `user_confirmations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`token` varchar(64) NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `token` (`token`),
CONSTRAINT `user_confirmations_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
ALTER TABLE
`users`
ADD
`active` tinyint(1) NOT NULL DEFAULT 0;

View File

@ -1,8 +0,0 @@
ALTER TABLE
`users`
ADD
`google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NULL DEFAULT NULL,
ADD
UNIQUE `google_sub` (`google_sub`),
MODIFY
`password` varchar(60) NULL DEFAULT NULL;

View File

@ -1,8 +0,0 @@
ALTER TABLE
`places`
ADD
`pov_heading` decimal(6, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_pitch` decimal(5, 3) NOT NULL DEFAULT 0.0,
ADD
`pov_zoom` decimal(5, 4) NOT NULL DEFAULT 0.0;

View File

@ -1,10 +0,0 @@
CREATE TABLE `user_password_resetters` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`token` varchar(32) CHARACTER SET ascii NOT NULL,
`expires` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `token` (`token`),
CONSTRAINT `user_password_resetters_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,5 +0,0 @@
UPDATE `user_confirmations` SET token=SUBSTRING(token, 1, 32);
ALTER TABLE `user_confirmations`
ADD `last_sent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `token` varchar(32) CHARACTER SET ascii NOT NULL;

View File

@ -1,2 +0,0 @@
ALTER TABLE `sessions`
MODIFY `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -1,4 +0,0 @@
UPDATE `sessions` SET id=SUBSTRING(id, 1, 32);
ALTER TABLE `sessions`
MODIFY `id` varchar(32) CHARACTER SET ascii NOT NULL;

View File

@ -1,2 +0,0 @@
ALTER TABLE `users`
ADD `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -1,9 +0,0 @@
CREATE TABLE `multi_rooms` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`room_id` varchar(6) NOT NULL,
`state` text NOT NULL,
`members` text NOT NULL,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `room_id` (`room_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,12 +0,0 @@
CREATE TABLE `user_played_place` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_id` int(10) unsigned NOT NULL,
`last_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`occurrences` int(10) NOT NULL DEFAULT 1,
PRIMARY KEY(`id`),
KEY `user_id` (`user_id`),
KEY `place_id` (`place_id`),
CONSTRAINT `user_played_place_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_played_place_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,54 +0,0 @@
CREATE TABLE `challenges` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`token` int(10) unsigned NOT NULL,
`time_limit` int(10) unsigned,
`time_limit_type` enum('game', 'round') NOT NULL DEFAULT 'game',
`no_move` tinyint(1) NOT NULL DEFAULT 0,
`no_pan` tinyint(1) NOT NULL DEFAULT 0,
`no_zoom` tinyint(1) NOT NULL DEFAULT 0,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `user_in_challenge` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`challenge_id` int(10) unsigned NOT NULL,
`current_round` smallint(5) signed NOT NULL DEFAULT 0,
`time_left` int(10) unsigned,
`is_owner` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `challenge_id` (`challenge_id`),
CONSTRAINT `user_in_challenge_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `user_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `place_in_challenge` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`place_id` int(10) unsigned NOT NULL,
`challenge_id` int(10) unsigned NOT NULL,
`round` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `place_id` (`place_id`),
KEY `challenge_id` (`challenge_id`),
CONSTRAINT `place_in_challenge_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`),
CONSTRAINT `place_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`),
CONSTRAINT `unique_order_in_challenge` UNIQUE (`round`, `challenge_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `guesses` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`place_in_challenge_id` int(10) unsigned NOT NULL,
`lat` decimal(8,6) NOT NULL,
`lng` decimal(9,6) NOT NULL,
`score` int(10) NOT NULL,
`distance` int(10) NOT NULL,
`time_spent` int(10),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `place_in_challenge_id` (`place_in_challenge_id`),
CONSTRAINT `guesses_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `guesses_place_in_challenge_id` FOREIGN KEY (`place_in_challenge_id`) REFERENCES `place_in_challenge` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -1,2 +0,0 @@
ALTER TABLE `maps`
ADD `unlisted` TINYINT(1) NOT NULL DEFAULT 0;

View File

@ -1,3 +0,0 @@
ALTER TABLE `users`
ADD `username` VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL AFTER `email`,
ADD UNIQUE `username` (`username`);

View File

@ -15,14 +15,6 @@ CREATE TABLE `maps` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `migrations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`type` enum('structure', 'data') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
DROP TABLE IF EXISTS `places`; DROP TABLE IF EXISTS `places`;
CREATE TABLE `places` ( CREATE TABLE `places` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,

View File

@ -1,25 +1,15 @@
version: '3' version: '3'
services: services:
app: app:
build: build: ./docker
context: .
dockerfile: docker/Dockerfile
target: mapg_dev
depends_on:
mariadb:
condition: service_healthy
ports: ports:
- 80:80 - 80:80
- 5000:5000
- 8090:8090
- 9229:9229
volumes: volumes:
- .:/var/www/mapguesser - .:/var/www/mapguesser
working_dir: /var/www/mapguesser links:
- 'mariadb'
mariadb: mariadb:
image: mariadb:10.3 image: mariadb:10.1
ports:
- 3306:3306
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
environment: environment:
@ -27,23 +17,5 @@ services:
MYSQL_DATABASE: 'mapguesser' MYSQL_DATABASE: 'mapguesser'
MYSQL_USER: 'mapguesser' MYSQL_USER: 'mapguesser'
MYSQL_PASSWORD: 'mapguesser' MYSQL_PASSWORD: 'mapguesser'
healthcheck:
test: ["CMD-SHELL", "mysqladmin -u $$MYSQL_USER -p$$MYSQL_PASSWORD ping -h localhost || exit 1"]
start_period: 5s
start_interval: 1s
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer:4.8.1-standalone
ports:
- 9090:8080
environment:
- ADMINER_DEFAULT_SERVER=mariadb
mail:
image: marcopas/docker-mailslurper:latest
ports:
- 8080:8080
- 8085:8085
volumes: volumes:
mysql: mysql:

View File

@ -1,44 +1,30 @@
FROM ubuntu:22.04 AS mapg_base FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
RUN apt update --fix-missing && apt install -y sudo curl git unzip mariadb-client nginx \ # Install Nginx, PHP and further necessary packages
php-apcu php8.1-cli php8.1-curl php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-zip php8.1-xml RUN apt update --fix-missing
RUN apt install -y curl git mariadb-client nginx \
php-apcu php-xdebug php7.4-cli php7.4-curl php7.4-fpm php7.4-mbstring php7.4-mysql php7.4-zip
# Configure Nginx with PHP
RUN mkdir -p /run/php RUN mkdir -p /run/php
COPY docker/configs/nginx.conf /etc/nginx/sites-available/default COPY configs/nginx.conf /etc/nginx/sites-available/default
RUN echo "xdebug.remote_enable = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_autostart = 1" >> /etc/php/7.4/mods-available/xdebug.ini
RUN echo "xdebug.remote_connect_back = 1" >> /etc/php/7.4/mods-available/xdebug.ini
COPY docker/scripts/install-composer.sh install-composer.sh # Install Composer
COPY scripts/install-composer.sh install-composer.sh
RUN ./install-composer.sh RUN ./install-composer.sh
COPY docker/scripts/install-nodejs.sh install-nodejs.sh # Install Node.js and required packages
RUN ./install-nodejs.sh RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN npm install -g uglify-js clean-css-cli svgo yarn RUN apt install -y nodejs
RUN npm install -g uglify-js clean-css-cli svgo
FROM mapg_base AS mapg_dev
RUN apt update --fix-missing && apt install -y php-xdebug
RUN echo "xdebug.remote_enable = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_autostart = 1" >> /etc/php/8.1/mods-available/xdebug.ini &&\
echo "xdebug.remote_connect_back = 1" >> /etc/php/8.1/mods-available/xdebug.ini
EXPOSE 80 EXPOSE 80
EXPOSE 5000 VOLUME /var/www/mapguesser
EXPOSE 8090
EXPOSE 9229
ENTRYPOINT docker/scripts/entry-point-dev.sh
FROM mapg_base AS mapg_release
RUN apt update --fix-missing && apt install -y cron
WORKDIR /var/www/mapguesser WORKDIR /var/www/mapguesser
COPY ./ /var/www/mapguesser
RUN rm -rf /var/www/mapguesser/.git
EXPOSE 80 ENTRYPOINT /usr/sbin/php-fpm7.4 -F & /usr/sbin/nginx -g 'daemon off;'
EXPOSE 8090
ENTRYPOINT docker/scripts/entry-point.sh

View File

@ -1,15 +1,11 @@
map $http_x_forwarded_proto $forwarded_scheme {
default $scheme;
http http;
https https;
}
server { server {
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server; listen [::]:80 default_server;
root /var/www/mapguesser/public; root /var/www/mapguesser/public;
index index.php index.html index.htm index.nginx-debian.html; index index.php index.html index.htm index.nginx-debian.html;
server_name mapguesser-dev.ch; server_name mapguesser-dev.ch;
location / { location / {
@ -18,8 +14,7 @@ server {
location ~ \.php$ { location ~ \.php$ {
include snippets/fastcgi-php.conf; include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_param REQUEST_SCHEME $forwarded_scheme;
} }
location ~ /\.ht { location ~ /\.ht {

View File

@ -1 +0,0 @@
0 * * * * /var/www/mapguesser/mapg db:maintain

View File

@ -1,40 +0,0 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
if [ -f .env ]; then
composer install
else
composer create-project
fi
echo "Installing NPM packages..."
(cd multi && npm install)
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Migrating DB..."
./mapg db:migrate
echo "Set runner user based on owner of .env..."
if ! getent group mapg; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID mapg
fi
if ! id -u mapg; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID mapg
fi
sed -i -e "s/^user = .*$/user = mapg/g" -e "s/^group = .*$/group = mapg/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
sudo -u mapg -g mapg /usr/bin/node --inspect=0.0.0.0:9229 multi &
wait -n
exit $?

View File

@ -1,32 +0,0 @@
#!/bin/bash
set -e
echo "Migrating DB..."
./mapg db:migrate
echo "Installing crontab..."
/usr/bin/crontab docker/scripts/cron
echo "Set runner user based on owner of .env..."
if ! getent group mapg; then
USER_GID=$(stat -c "%g" .env)
groupadd --gid $USER_GID mapg
fi
if ! id -u mapg; then
USER_UID=$(stat -c "%u" .env)
useradd --uid $USER_UID --gid $USER_GID mapg
fi
chown mapg:mapg cache
sed -i -e "s/^user = .*$/user = mapg/g" -e "s/^group = .*$/group = mapg/g" /etc/php/8.1/fpm/pool.d/www.conf
set +e
/usr/sbin/cron -f &
/usr/sbin/php-fpm8.1 -F &
/usr/sbin/nginx -g 'daemon off;' &
sudo -u mapg -g mapg /usr/bin/node multi &
wait -n
exit $?

View File

@ -1,14 +0,0 @@
#!/bin/sh
set -e
apt update
apt install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=18
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
apt update
apt install -y nodejs

View File

@ -1,30 +0,0 @@
#!/bin/bash
set -e
echo "Installing Composer packages..."
composer create-project --no-dev
echo "Installing NPM packages..."
(cd multi && npm install)
echo "Installing Yarn packages..."
(cd public/static && yarn install)
echo "Updating version info..."
VERSION=$(git describe --tags --always --match "Release_*" HEAD)
REVISION=$(git rev-parse --short HEAD)
REVISION_DATE=$(git show -s --format=%aI HEAD)
sed -i -E "s/const VERSION = '(.*)';/const VERSION = '${VERSION}';/" main.php
sed -i -E "s/const REVISION = '(.*)';/const REVISION = '${REVISION}';/" main.php
sed -i -E "s/const REVISION_DATE = '(.*)';/const REVISION_DATE = '${REVISION_DATE}';/" main.php
echo "Minifying JS, CSS and SVG files..."
find public/static/js -type f -iname '*.js' -exec uglifyjs {} -c -m -o {} \;
find public/static/css -type f -iname '*.css' -exec cleancss {} -o {} \;
find public/static/img -type f -iname '*.svg' -exec svgo {} -o {} \;
echo "Linking view files..."
./mapg view:link
rm .env

View File

@ -1,13 +0,0 @@
Hi,
<br><br>
You recently requested password reset on {{APP_NAME}} with this email address ({{EMAIL}}).
To reset the password to your account, please click on the following link:<br>
<a href="{{RESET_LINK}}" title="Reset password">{{RESET_LINK}}</a>
<br><br>
You can reset your password with this link util {{EXPIRES}}.
<br><br>
If you did not requested password reset, no further action is required, your account is not touched.
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,9 +0,0 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this Google account ({{EMAIL}}).
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,18 +0,0 @@
Hi,
<br><br>
You recently signed up on {{APP_NAME}} with this email address ({{EMAIL}}).
To activate your account, please click on the following link:<br>
<a href="{{ACTIVATE_LINK}}" title="Account activation">{{ACTIVATE_LINK}}</a>
<br><br>
You can activate your account until {{ACTIVATABLE_UNTIL}}.
If you don't activate your account, your email address will be permanently deleted after this point of time.
<br><br>
If you did not sign up on {{APP_NAME}} or changed your mind, no further action is required.
However if you want to immediately delete your email address, please click on the following link:<br>
<a href="{{CANCEL_LINK}}" title="Sign up cancellation">{{CANCEL_LINK}}</a>
<br><br>
Have fun on {{APP_NAME}}!
<br><br>
Regards,<br>
{{APP_NAME}}<br>
<a href="{{BASE_URL}}" title="{{APP_NAME}}">{{BASE_URL}}</a>

View File

@ -1,25 +1,27 @@
<?php <?php
define('SCRIPT_STARTED', hrtime(true));
require 'vendor/autoload.php'; require 'vendor/autoload.php';
const ROOT = __DIR__; const ROOT = __DIR__;
const VERSION = '';
const REVISION = '';
const REVISION_DATE = '';
$dotenv = Dotenv\Dotenv::createImmutable(ROOT); $dotenv = Dotenv\Dotenv::createImmutable(ROOT);
$dotenv->load(); $dotenv->load();
class Container if (!empty($_ENV['DEV'])) {
{ error_reporting(E_ALL);
static SokoWeb\Interfaces\Database\IConnection $dbConnection;
static SokoWeb\Interfaces\PersistentData\IPersistentDataManager $persistentDataManager; ini_set('display_errors', '1');
static SokoWeb\Interfaces\Routing\IRouteCollection $routeCollection; } else {
static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler; ini_set('display_errors', '0');
static SokoWeb\Interfaces\Request\IRequest $request;
} }
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); class Container
Container::$persistentDataManager = new SokoWeb\PersistentData\PersistentDataManager(Container::$dbConnection); {
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
static MapGuesser\Routing\RouteCollection $routeCollection;
}
Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
Container::$routeCollection = new MapGuesser\Routing\RouteCollection();
session_start();

13
mapg
View File

@ -1,13 +0,0 @@
#!/usr/bin/env php
<?php
require 'main.php';
$app = new Symfony\Component\Console\Application('MapGuesser Console', '');
$app->add(new MapGuesser\Cli\MigrateDatabaseCommand());
$app->add(new MapGuesser\Cli\AddUserCommand());
$app->add(new MapGuesser\Cli\LinkViewCommand());
$app->add(new MapGuesser\Cli\MaintainDatabaseCommand());
$app->run();

View File

@ -1,369 +0,0 @@
'use strict';
process.title = 'mapguesser-multi';
class MultiGame {
static ROUND_TIMEOUT_DEFAULT = 120000;
static ROUND_TIMEOUT_MINIMUM = 15000;
static ROUND_TIMEOUT_DIVIDER = 1.5;
static ROUND_TIMEOUT_OFFSET = 500;
constructor() {
this.rooms = new Map();
}
cleanupRooms() {
this.rooms.forEach(function (room, roomId) {
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
if (room.updated < lastValidDate) {
this.rooms.delete(roomId);
}
});
}
connectToRoom(roomId, token, connection) {
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
return;
}
var room = this.rooms.get(roomId)
var member = room.members.get(token);
member.connection = connection;
this._sendInitialData(room, member, token);
}
createRoom(roomId) {
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
return { ok: true };
}
joinRoom(roomId, token, userName) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
if (room.members.has(token)) {
return { error: 'member_already_joined' };
}
var data = { userName: userName };
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'member_joined', data);
});
room.members.set(token, { userName: userName, connection: null });
return { ok: true };
}
startGame(roomId, places) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var rounds = [];
places.forEach(function (place) {
rounds.push({
place: place,
results: new Map(),
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
timeoutStarted: null,
timeoutHandler: null
})
});
room.rounds = rounds;
this.nextRound(roomId, 0);
return { ok: true };
}
guess(roomId, token, guessPosition, distance, score) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var round = room.rounds[room.currentRound];
if (round.results.has(token)) {
return { error: 'already_guessed' };
}
var member = room.members.get(token);
var allResults = this._collectResultsInRound(room, round);
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
this._setNewTimeout(room, round);
return { allResults: allResults };
}
nextRound(roomId, currentRound) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
room.currentRound = currentRound;
var round = room.rounds[room.currentRound];
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
var data = {};
data.place = { panoId: round.place.panoId, pov: round.place.pov };
data.timeout = round.timeout;
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'new_round', data);
});
return { ok: true };
}
_setNewTimeout(room, round) {
clearTimeout(round.timeoutHandler);
if (room.members.size === round.results.size) {
round.timeout = 0;
round.timeoutStarted = new Date();
this._endRound(room, round);
} else {
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
}
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
this._broadcastTimeout(room, round);
}
}
_endRound(room, round) {
var allResults = this._collectResultsInRound(room, round);
var self = this;
room.members.forEach(function (member, token) {
var result = { guessPosition: null, distance: null, score: 0 };
if (round.results.has(token)) {
result = round.results.get(token);
} else {
round.results.set(token, result);
}
var data = { position: round.place.position, result: result, allResults: allResults };
self._sendToMember(member, 'end_round', data);
});
}
_sendInitialData(room, member, token) {
var data = {};
if (room.currentRound >= 0) {
var round = room.rounds[room.currentRound];
data.place = round.place;
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
}
data.history = [];
for (var i = 0; i <= room.currentRound; ++i) {
var round = room.rounds[i];
if (i === room.currentRound && !round.results.has(token)) {
continue;
}
var result = { guessPosition: null, distance: null, score: 0 };
var allResults = [];
round.results.forEach(function (currentResult, currentToken) {
if (token === currentToken) {
result = currentResult;
return;
}
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
});
data.history.push({
position: round.place.position,
result: result,
allResults: allResults
});
}
data.members = [];
room.members.forEach(function (currentMember) {
data.members.push({ userName: currentMember.userName, me: member === currentMember });
});
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
this._sendToMember(member, 'initialize', data);
}
_collectResultsInRound(room, round) {
var results = [];
round.results.forEach(function (result, token) {
results.push({
userName: room.members.get(token).userName,
guessPosition: result.guessPosition,
distance: result.distance,
score: result.score
});
});
return results;
}
_broadcastTimeout(room, round) {
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
});
}
_broadcastGuess(room, userName, guessPosition, distance, score) {
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
var round = room.rounds[room.currentRound];
var self = this;
room.members.forEach(function (member, token) {
if (!round.results.has(token)) {
return;
}
self._sendToMember(member, 'guess', data);
});
}
_sendToMember(member, type, data) {
if (!member.connection) {
return;
}
if (member.connection.readyState !== ws.OPEN) {
member.connection = null;
return;
}
member.connection.send(JSON.stringify({ type: type, data: data }));
}
}
require('dotenv').config();
var
net = require('net'),
ws = require('ws');
var multiGame = new MultiGame();
//TODO: following should be in a separate class/function
var tcpServer = net.createServer(function (socket) {
socket.on('data', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
var response;
switch (data.func) {
case 'create_room':
response = multiGame.createRoom(data.args.roomId);
break;
case 'join_room':
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
break;
case 'start_game':
response = multiGame.startGame(data.args.roomId, data.args.places);
break
case 'guess':
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
break;
case 'next_round':
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
break;
}
socket.write(JSON.stringify(response));
socket.end();
});
});
tcpServer.on('listening', function () {
console.log('[INFO] TCP server started');
});
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
wsServer.on('connection', function (connection, request) {
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
connection.on('message', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
switch (data.func) {
case 'connect_to_room':
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
break;
}
});
connection.on('close', function () {
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
});
});
wsServer.on('listening', function () {
console.log('[INFO] WS server started');
});
setInterval(function () {
multiGame.cleanupRooms();
}, 24 * 60 * 60 * 1000);

View File

@ -1,43 +0,0 @@
{
"name": "mapguesser-multi",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mapguesser-multi",
"license": "GNU AGPL 3.0",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
}
},
"node_modules/dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"engines": {
"node": ">=8"
}
},
"node_modules/ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"name": "mapguesser-multi",
"version": "",
"description": "MapGuesser Application - Multiplayer",
"main": "index.js",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
},
"scripts": {},
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
"license": "GNU AGPL 3.0"
}

View File

@ -1,9 +0,0 @@
parameters:
level: 5
checkMissingIterableValueType: false
paths:
- main.php
- mapg
- web.php
- src
- tests

View File

@ -1,3 +1,39 @@
<?php <?php
require '../web.php'; require '../main.php';
// very basic routing
$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'];
$method = strtolower($_SERVER['REQUEST_METHOD']);
$url = substr($_SERVER['REQUEST_URI'], strlen('/'));
if (($pos = strpos($url, '?')) !== false) {
$url = substr($url, 0, $pos);
}
$url = rawurldecode($url);
Container::$routeCollection->get('index', '', [MapGuesser\Controller\HomeController::class, 'getIndex']);
Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
$routeCollection->get('position-json', '{mapId}/position.json', [MapGuesser\Controller\PositionController::class, 'getPosition']);
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\PositionController::class, 'evaluateGuess']);
});
$match = Container::$routeCollection->match($method, explode('/', $url));
if ($match !== null) {
list($route, $params) = $match;
$response = $route->callController($params);
if ($response instanceof MapGuesser\Interfaces\Response\IContent) {
header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
echo $response->render();
} elseif ($response instanceof MapGuesser\Interfaces\Response\IRedirect) {
header('Location: ' . $host . '/' . $response->getUrl(), true, $response->getHttpCode());
}
} else {
header('Content-Type: text/html; charset=UTF-8', true, 404);
require ROOT . '/views/error/404.php';
}

View File

@ -1 +0,0 @@
node_modules

View File

@ -1,301 +0,0 @@
#panorama {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
#panoCover {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 4;
}
#panningBlockerCover {
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0;
z-index: 3;
}
#guess {
position: absolute;
bottom: 30px;
right: 20px;
z-index: 3;
}
#guess.result {
z-index: 4;
}
#guess>#continueButtonContainer {
display: none;
}
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
display: none;
}
#guess.result>#continueButtonContainer {
display: block;
}
#map {
width: 100%;
border-radius: 3px;
}
#guess.result>#map {
height: calc(100% - 170px);
}
#resultInfo {
margin-top: 5px;
width: 100%;
height: 120px;
padding: 5px 20px;
text-align: center;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 3px;
display: none;
}
#guess.result>#resultInfo {
display: block;
}
#resultInfo>div {
width: 100%;
height: 33.33%;
display: flex;
justify-content: center;
align-items: center;
}
#resultInfo p {
font-size: 24px;
line-height: 1;
}
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
display: none;
}
#scoreBarBase {
height: 24px;
margin: 0 auto;
background-color: #eeeeee;
border-radius: 3px;
}
#scoreBar {
width: 0;
height: 100%;
border-radius: 3px;
transition-property: width;
transition-duration: 2.0s;
}
#showSummaryButton, #startNewGameButton {
display: none;
}
#startMultiGameButton {
display: none;
}
#players > p {
font-size: 14px;
font-weight: bold;
}
#countdown {
position: absolute;
top: 5px;
right: 5px;
height: 28px;
line-height: 28px;
padding: 0 8px;
background-color: #eeeeee;
border: solid 1px #555555;
border-radius: 3px;
opacity: 0.95;
z-index: 5;
visibility: hidden;
}
#countdown.yellow {
background-color: #f7c789;
border: solid 1px #e8a349;
}
#countdown.red {
background-color: #f7a5a5;
border: solid 1px #aa5e5e;
}
#countdown p {
font-size: 16px;
line-height: inherit;
}
#countdown.yellow p {
color: #9c4308;
}
#countdown.red p {
color: #701919;
}
#navigation {
z-index: 2;
}
#goToStart {
display: none;
}
#highscoresTable {
margin: 1em;
border-collapse: collapse;
width: 90%;
}
#highscoresTable td, #highscoresTable th {
border: 1px solid #ddd;
padding: 8px;
}
#highscoresTable tr:nth-child(even) {
background-color: #f2f2f2;
}
#highscoresTable tr:hover {
background-color: #ddd;
}
#highscoresTable th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #e8a349;
color: white;
}
#highscoresTable tr.ownPlayer {
font-weight: 500;
}
@media screen and (max-width: 899px) {
.hideOnNarrowScreen {
display: none;
}
}
@media screen and (max-width: 599px) {
#mapName {
display: none;
}
#showGuessButtonContainer {
position: absolute;
left: 65px;
bottom: 30px;
right: 20px;
z-index: 2;
}
#guess {
top: 10px;
left: 20px;
opacity: 0.95;
visibility: hidden;
}
#map {
height: calc(100% - 90px);
}
#scoreBarBase {
width: 100%;
}
#navigation {
bottom: 25px;
left: 10px;
}
}
@media screen and (min-width: 600px) {
#showGuessButtonContainer {
display: none;
}
#guess {
width: 500px;
height: 375px;
opacity: 0.95;
}
#guess.adapt {
top: initial;
width: 250px;
height: 200px;
opacity: 0.5;
transition-property: width, height, opacity;
transition-duration: 0.1s;
transition-delay: 0.8s;
}
#guess.adapt:hover {
width: 500px;
height: 375px;
opacity: 0.95;
transition-delay: 0s;
}
#closeGuessButtonContainer {
display: none;
}
#map {
height: calc(100% - 45px);
}
#guess.result {
width: initial;
height: initial;
top: 10px;
left: 50px;
right: 50px;
bottom: 50px;
}
#scoreBarBase {
width: 60%;
}
#navigation {
bottom: 50px;
left: 20px;
}
@media screen and (max-height: 424px) {
#guess {
top: 10px;
height: initial;
}
#guess.adapt:hover {
top: 10px;
height: initial;
}
#guess.result {
left: 20px;
right: 20px;
bottom: 30px;
}
#navigation {
bottom: 30px;
left: 10px;
}
}
}

View File

@ -1,108 +0,0 @@
.map {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
#mapSelection img {
display: inline;
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
/* modify the cursor for the Leaflet map */
.leaflet-container {
cursor: crosshair;
}
#panorama {
position: absolute;
z-index: 1;
visibility: hidden;
}
#noPano {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 2;
visibility: hidden;
background: #cccccc;
}
#noPano>p {
text-align: center;
}
#control {
position: absolute;
top: 10px;
right: 10px;
width: 125px;
z-index: 3;
}
#placeControl {
position: absolute;
right: 10px;
z-index: 3;
width: 100px;
visibility: hidden;
}
#overlayControl {
bottom: 20px;
right: 10px;
z-index: 3;
}
#deleteButton {
display: none;
}
@media screen and (max-width: 999px) and (min-height: 600px) {
#map.selected {
height: 50%;
}
#panorama, #noPano {
left: 0;
bottom: 0;
right: 0;
height: 50%;
}
#placeControl {
top: calc(50% + 10px);
}
.hideOnMobile {
display: none;
}
}
@media screen and (min-width: 1000px), (max-height: 599px) {
#map.selected {
top: 0;
bottom: 0;
left: 0;
width: 50%;
}
#panorama, #noPano {
top: 0;
bottom: 0;
right: 0;
width: 50%;
}
#placeControl {
top: 10px;
}
#modified.selected {
right: calc(50% + 10px);
}
#control.selected {
right: calc(50% + 10px);
}
}

View File

@ -12,30 +12,16 @@ html, body {
padding: 0; padding: 0;
} }
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner { button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0; padding: 0;
border: 0; border: 0;
} }
/* to be compatible with browsers that don't know <main> */ p, h1, h2, button, a {
main {
display: block;
}
::selection {
background-color: #28a745;
color: #ffffff;
}
p, h1, h2, h3, input, textarea, select, button, a, table, label {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
} }
h1, h2, h3 { h1, h2 {
font-weight: 500; font-weight: 500;
} }
@ -51,15 +37,11 @@ h1>a:hover, h1>a:focus {
text-decoration: none; text-decoration: none;
} }
h2, header.small h1 { h2, div.header.small h1 {
font-size: 24px; font-size: 24px;
} }
h3 { p, h2 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%; line-height: 150%;
} }
@ -87,17 +69,16 @@ sub {
bottom: -0.4em; bottom: -0.4em;
} }
hr { .mono {
border: solid #bbbbbb 1px; font-family: 'Roboto Mono', monospace;
margin: 10px 0;
} }
.bold { .bold {
font-weight: 500; font-weight: 500;
} }
p.small, span.small { .small {
font-size: 14px; font-size: 12px;
} }
.justify { .justify {
@ -108,31 +89,11 @@ p.small, span.small {
margin-top: 10px; margin-top: 10px;
} }
.marginLeft {
margin-left: 10px;
}
.marginBottom { .marginBottom {
margin-bottom: 10px; margin-bottom: 10px;
} }
.marginRight { svg.inline {
margin-right: 10px;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
svg.inline, img.inline {
display: inline;
width: 1em;
height: 1em;
margin-right: 0.3em;
vertical-align: -0.15em; vertical-align: -0.15em;
} }
@ -161,27 +122,6 @@ button, a.button {
line-height: 35px; line-height: 35px;
} }
button.small, div.inputWithButton>button {
font-size: 14px;
padding: 0 12px;
height: 32px;
line-height: 32px;
}
button.small {
height: 32px;
line-height: 32px;
}
div.inputWithButton>button {
border-radius: 2px;
height: 27px;
line-height: 27px;
width: 75px;
margin-left: -79px;
vertical-align: 2px;
}
button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus { button:enabled:hover, button:enabled:focus, a.button:hover, a.button:focus {
background-color: #29457f; background-color: #29457f;
outline: none; outline: none;
@ -199,162 +139,15 @@ button.fullWidth, a.button.fullWidth {
width: 100%; width: 100%;
} }
button.noLeftRadius, a.button.noLeftRadius {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
button.noRightRadius, a.button.noRightRadius {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
button.gray, a.button.gray { button.gray, a.button.gray {
background-color: #808080; background-color: #808080;
} }
button.gray:enabled:hover, button.gray:enabled:focus, a.button.gray:hover, a.button.gray:focus { button.gray:hover, button.gray:focus, a.button.gray:hover, a.button.gray:focus {
background-color: #555555; background-color: #555555;
} }
button.red, a.button.red { div.header {
background-color: #aa5e5e;
}
button.red:enabled:hover, button.red:enabled:focus, a.button.red:hover, a.button.red:focus {
background-color: #7f2929;
}
button.yellow, a.button.yellow {
background-color: #e8a349;
}
button.yellow:enabled:hover, button.yellow:enabled:focus, a.button.yellow:hover, a.button.yellow:focus {
background-color: #c37713;
}
button.green, a.button.green {
background-color: #28a745;
}
button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.button.green:focus {
background-color: #1b7d31;
}
input.text, select, textarea {
background-color: #f9fafb;
border: solid #c8d2e1 1px;
border-radius: 2px;
box-sizing: border-box;
font-size: 15px;
font-weight: 300;
}
input.text, select {
height: 30px;
line-height: 30px;
padding: 0 5px;
}
input[type=checkbox], input[type=radio] {
margin-right: 0.5em;
}
textarea {
padding: 5px;
resize: none;
}
input.text.big, select.big, textarea.big, div.inputWithButton>input.text {
font-size: 18px;
}
input.text.big, select.big, div.inputWithButton>input.text {
height: 35px;
line-height: 35px;
padding: 0 6px;
}
textarea.big {
padding: 6px;
}
input.fullWidth, select.fullWidth, textarea.fullWidth {
display: block;
width: 100%;
}
input.text:disabled, select:disabled, textarea:disabled {
background-color: #dfdfdf;
border: solid #dfdfdf 1px;
color: #000000;
}
input.text:focus, select:focus, textarea:focus {
background-color: #ffffff;
border: solid #29457f 2px;
outline: none;
}
input.text:focus, select:focus {
padding: 0 4px;
}
textarea:focus {
padding: 4px;
}
input.text.big:focus, select.big:focus {
padding: 0 5px;
}
div.inputWithButton>input.text {
width: 100%;
padding: 0 83px 0 6px;
}
div.inputWithButton>input.text:focus {
padding: 0 82px 0 5px;
}
textarea.big:focus {
padding: 5px;
}
div.modal {
position: fixed;
background-color: #ffffff;
border-radius: 3px;
box-sizing: border-box;
overflow-y: auto;
z-index: 6;
visibility: hidden;
}
#cover {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 5;
visibility: hidden;
}
p.error, p.formError {
color: #7f2929;
font-weight: 500;
}
p.formError {
display: none;
}
header {
display: grid;
grid-template-columns: auto auto;
background-color: #333333; background-color: #333333;
height: 50px; height: 50px;
line-height: 50px; line-height: 50px;
@ -362,52 +155,20 @@ header {
color: white; color: white;
} }
header.small { div.header>div.grid {
display: grid;
grid-template-columns: auto auto;
}
div.header.small {
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
} }
header>p { div.main {
line-height: inherit;
text-align: right;
}
header>p>span {
padding-left: 6px;
}
header>p>span>a:link, header>p>span>a:visited, footer>p>a:link, footer>p>a:visited {
color: inherit;
}
header>p>span:not(:last-child) {
border-right: solid white 1px;
padding-right: 6px;
}
main {
background-color: #ffffff;
padding: 6px 12px; padding: 6px 12px;
} }
main.full {
position: relative;
width: 100%;
height: calc(100% - 40px);
padding: 0;
}
footer {
background-color: #444444;
padding: 6px 12px;
color: white;
text-align: center;
}
footer>p {
font-size: 13px;
}
div.buttonContainer { div.buttonContainer {
height: 35px; height: 35px;
} }
@ -416,163 +177,287 @@ div.buttonContainer>button {
margin: 0 auto; margin: 0 auto;
} }
#cookiesNotice { div.mapContainer {
position: fixed; display: grid;
left: 0; }
bottom: 0;
right: 0; div.mapItem {
margin: 20px; width: 350px;
background-color: #eeeeee; background-color: #eeeeee;
border: solid #888888 1px;
border-radius: 3px; border-radius: 3px;
padding: 10px; margin: 10px auto;
text-align: center; }
z-index: 10;
div.mapItem>div.title {
background-color: #28a745;
color: white;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 4px 8px;
}
div.mapItem>div.title>p.title {
font-weight: 500;
font-size: 18px;
}
div.mapItem>img {
width: 100%;
}
div.mapItem>div.inner {
padding: 8px;
}
div.mapItem>div.inner>div.info {
display: grid;
grid-template-columns: auto auto;
}
div.mapItem>div.inner>div.info>p:nth-child(1) {
text-align: left;
}
div.mapItem>div.inner>div.info>p:nth-child(2) {
text-align: right;
} }
#loading { #loading {
position: fixed; position: absolute;
width: 64px; width: 64px;
height: 64px; height: 64px;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-top: -32px; margin-top: -32px;
margin-left: -32px; margin-left: -32px;
z-index: 7; z-index: 5;
visibility: hidden; visibility: hidden;
} }
div.box { #roundInfo {
width: 576px; line-height: inherit;
text-align: right;
}
#roundInfo p {
font-size: 16px;
line-height: inherit;
}
#panorama {
width: 100%;
height: calc(100% - 40px);
z-index: 1;
}
#cover {
position: absolute;
top: 40px;
left: 0;
bottom: 0;
right: 0;
background-color: #000000;
opacity: 0.5;
z-index: 3;
}
#guess {
position: absolute;
bottom: 30px;
right: 20px;
z-index: 2;
}
#guess.result {
z-index: 4;
}
#guess>#continueButtonContainer {
display: none;
}
#guess.result>#closeGuessButtonContainer, #guess.result>#guessButtonContainer {
display: none;
}
#guess.result>#continueButtonContainer {
display: block;
}
#map {
width: 100%;
border-radius: 3px;
}
#guess.result>#map {
height: calc(100% - 170px);
}
#resultInfo {
margin-top: 5px;
width: 100%;
height: 120px;
padding: 5px 20px;
text-align: center;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 3px;
display: none;
}
#guess.result>#resultInfo {
display: block;
}
#resultInfo>div {
width: 100%;
height: 33.33%;
display: flex;
justify-content: center;
align-items: center;
}
#resultInfo p {
font-size: 24px;
line-height: 1;
}
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
display: none;
}
#scoreBarBase {
height: 24px;
margin: 0 auto;
background-color: #eeeeee; background-color: #eeeeee;
border-radius: 3px; border-radius: 3px;
margin: 10px auto;
padding: 10px;
box-sizing: border-box;
} }
.circleControl { #scoreBar {
position: absolute; width: 0;
width: 60px;
bottom: 20px;
right: 10px;
}
.circleControl .controlItem {
position: relative;
height: 60px;
margin-top: 10px;
opacity: 70%;
cursor: pointer;
}
.circleControl .controlItem:hover {
opacity: 100%;
}
.circleControl .controlItem div {
position: absolute;
width: 100%;
height: 100%; height: 100%;
border-radius: 3px;
transition-property: width;
transition-duration: 2.0s;
} }
.circleControl .controlBackground { #showSummaryButton, #startNewGameButton {
width: 100%; display: none;
height: 100%;
opacity: 50%;
} }
.circleControl .controlIcon { @media screen and (min-width: 1504px) {
width: 75%; div.mapContainer {
height: 75%; grid-template-columns: auto auto auto auto;
margin: auto; }
margin-top: 50%; }
transform: translateY(-50%);
@media screen and (min-width: 1134px) and (max-width: 1503px) {
div.mapContainer {
grid-template-columns: auto auto auto;
}
}
@media screen and (min-width: 764px) and (max-width: 1133px) {
div.mapContainer {
grid-template-columns: auto auto;
}
}
@media screen and (max-width: 763px) {
div.mapContainer {
grid-template-columns: auto;
}
}
@media screen and (max-width: 374px) {
div.mapItem {
width: initial;
}
} }
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
header h1 span { div.header.small h1 span {
display: none; display: none;
} }
footer>p:not(:first-child) { button {
margin-top: 4px;
}
button, a.button {
padding: 0; padding: 0;
width: 100%; width: 100%;
} }
button.marginLeft, a.button.marginLeft { #showGuessButtonContainer {
margin-left: 0; position: absolute;
}
button.marginRight, a.button.marginRight {
margin-right: 0;
}
div.modal {
left: 20px; left: 20px;
bottom: 30px;
right: 20px; right: 20px;
padding-left: 15px; z-index: 2;
padding-right: 15px;
} }
div.box { #guess {
width: initial; top: 50px;
left: 20px;
opacity: 0.95;
visibility: hidden;
} }
.circleControl { #map {
width: 45px; height: calc(100% - 90px);
} }
.circleControl .controlItem { #scoreBarBase {
height: 45px; width: 100%;
} }
} }
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
footer>p { #showGuessButtonContainer {
display: inline; display: none;
} }
footer>p:not(:first-child) { #guess {
padding-left: 6px; width: 500px;
height: 375px;
opacity: 0.95;
} }
footer>p:not(:last-child) { #guess.adapt {
border-right: solid white 1px; top: initial;
padding-right: 6px; width: 250px;
height: 200px;
opacity: 0.5;
transition-property: width, height, opacity;
transition-duration: 0.1s;
transition-delay: 0.8s;
} }
div.modal { #guess.adapt:hover {
width: 540px; width: 500px;
left: 50%; height: 375px;
margin-left: -270px; opacity: 0.95;
padding-left: 20px; transition-delay: 0s;
padding-right: 20px;
} }
} #closeGuessButtonContainer {
display: none;
@media screen and (max-height: 399px) {
div.modal {
top: 20px;
max-height: calc(100% - 40px);
padding-top: 10px;
padding-bottom: 10px;
} }
.circleControl { #map {
width: 45px; height: calc(100% - 45px);
} }
.circleControl .controlItem { #guess.result {
height: 45px; width: initial;
} height: initial;
}
@media screen and (min-height: 400px) and (max-height: 499px) {
div.modal {
top: 50px; top: 50px;
max-height: calc(100% - 100px); left: 50px;
padding-top: 15px; right: 50px;
padding-bottom: 15px; bottom: 50px;
} }
} #scoreBarBase {
width: 60%;
@media screen and (min-height: 500px) { }
div.modal { @media screen and (max-height: 424px) {
top: 75px; #guess {
max-height: calc(100% - 150px); top: 50px;
padding-top: 15px; height: initial;
padding-bottom: 15px; }
#guess.adapt:hover {
top: 50px;
height: initial;
}
#guess.result {
left: 20px;
right: 20px;
bottom: 30px;
}
} }
} }

View File

@ -1,114 +0,0 @@
#mapContainer {
display: grid;
}
div.mapItem {
width: 350px;
margin: 10px auto;
}
div.mapItem.new {
display: flex;
justify-content: center;
align-items: center;
}
div.mapItem.unlisted {
opacity: 0.6;
}
div.mapItem>div.title {
background-color: #28a745;
color: white;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 0 8px;
height: 35px;
line-height: 35px;
}
div.mapItem>div.title>p {
line-height: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.mapItem>div.title>p.title {
font-weight: 500;
font-size: 18px;
}
div.mapItem>div.imgContainer {
width: 100%;
padding-top: 50%;
background-color: #cccccc;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
div.mapItem>div.inner {
background-color: #eeeeee;
padding: 10px 8px;
box-sizing: border-box;
}
div.mapItem>div.inner>div.info {
display: grid;
grid-template-columns: auto auto;
}
div.mapItem>div.inner>div.description {
display: flex;
justify-content: center;
align-items: center;
}
div.mapItem>div.inner>div.info>p:nth-child(1) {
text-align: left;
}
div.mapItem>div.inner>div.info>p:nth-child(2) {
text-align: right;
}
div.mapItem>div.buttonContainer {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
}
#timeLimitType {
margin-left: 1.5em;
}
@media screen and (min-width: 1504px) {
#mapContainer {
grid-template-columns: auto auto auto auto;
}
}
@media screen and (min-width: 1134px) and (max-width: 1503px) {
#mapContainer {
grid-template-columns: auto auto auto;
}
}
@media screen and (min-width: 764px) and (max-width: 1133px) {
#mapContainer {
grid-template-columns: auto auto;
}
}
@media screen and (max-width: 763px) {
#mapContainer {
grid-template-columns: auto;
}
}
@media screen and (max-width: 374px) {
div.mapItem {
width: 100%;
}
}

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
<circle cx="50%" cy="50%" r="50%" fill="black" />
</svg>

Before

Width:  |  Height:  |  Size: 146 B

View File

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon id="needleNorth" points="50,0 33,50 66,50" style="fill:red;stroke:black;stroke-width:1" />
<polygon id="needleSouth" points="50,100 33,50 66,50" style="fill:white;stroke:black;stroke-width:1" />
Sorry, your browser does not support inline SVG.
</svg>

Before

Width:  |  Height:  |  Size: 328 B

BIN
public/static/img/favicon/16x16.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/favicon/192x192.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/favicon/32x32.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/favicon/96x96.png (Stored with Git LFS)

Binary file not shown.

View File

@ -1,2 +0,0 @@
The PNGs in this folder are generated from '../icon.svg'.
Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'.

View File

@ -1,3 +1,4 @@
<!-- The PNGs in this folder are generated from this SVG. -->
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. --> <!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 16 16" fill="#28a745" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" /> <path fill-rule="evenodd" d="M15.817.613A.5.5 0 0 1 16 1v13a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 14.51l-4.902.98A.5.5 0 0 1 0 15V2a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0l4.902.98 4.902-.98a.5.5 0 0 1 .415.103zM10 2.41l-4-.8v11.98l4 .8V2.41zm1 11.98l4-.8V1.61l-4 .8v11.98zm-6-.8V1.61l-4 .8v11.98l4-.8z" />

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 591 B

View File

@ -1,5 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="#3183ce" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 556 B

View File

@ -1,4 +0,0 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 530 B

BIN
public/static/img/markers/m1.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/markers/m2.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/markers/m3.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/markers/m4.png (Stored with Git LFS)

Binary file not shown.

BIN
public/static/img/markers/m5.png (Stored with Git LFS)

Binary file not shown.

View File

@ -1,10 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#3183ce"
fill-rule="evenodd"
stroke="#19456d"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

Before

Width:  |  Height:  |  Size: 529 B

View File

@ -1,16 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#3183ce"
fill-rule="evenodd"
stroke="#19456d"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
<circle
fill="#19456d"
fill-rule="evenodd"
cx="6"
cy="6"
r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 639 B

View File

@ -1,10 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#828282"
fill-rule="evenodd"
stroke="#383838"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

Before

Width:  |  Height:  |  Size: 529 B

View File

@ -1,16 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#828282"
fill-rule="evenodd"
stroke="#383838"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
<circle
fill="#383838"
fill-rule="evenodd"
cx="6"
cy="6"
r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 639 B

View File

@ -1,10 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#28a745"
fill-rule="evenodd"
stroke="#285624"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

Before

Width:  |  Height:  |  Size: 529 B

View File

@ -1,16 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#28a745"
fill-rule="evenodd"
stroke="#285624"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
<circle
fill="#285624"
fill-rule="evenodd"
cx="6"
cy="6"
r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 639 B

View File

@ -1,16 +0,0 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#d24d4d"
fill-rule="evenodd"
stroke="#752929"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
<circle
fill="#752929"
fill-rule="evenodd"
cx="6"
cy="6"
r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 639 B

View File

@ -1,5 +0,0 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path d="m14.12 10.163 1.715.858c.22.11.22.424 0 .534L8.267 15.34a.598.598 0 0 1-.534 0L.165 11.555a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.66zM7.733.063a.598.598 0 0 1 .534 0l7.568 3.784a.3.3 0 0 1 0 .535L8.267 8.165a.598.598 0 0 1-.534 0L.165 4.382a.299.299 0 0 1 0-.535L7.733.063z"/>
<path d="m14.12 6.576 1.715.858c.22.11.22.424 0 .534l-7.568 3.784a.598.598 0 0 1-.534 0L.165 7.968a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.659z"/>
</svg>

Before

Width:  |  Height:  |  Size: 718 B

View File

@ -1,69 +0,0 @@
var Account = {
original: null,
countdown: null,
openGoogleAuthenticate: function () {
window.open('/account/googleAuthenticate', 'googleAuthenticate', 'height=600,width=600')
},
authenticatedWithGoogleCallback: function (authenticatedWithGoogleUntil) {
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
Account.original = {
type: password.type,
placeholder: password.placeholder,
required: password.required,
disabled: password.disabled
};
password.type = 'text';
password.placeholder = 'Authenticated!'
password.value = '';
password.required = false;
password.disabled = true;
button.disabled = true;
Account.countdown = setInterval(function () {
var timeLeft = Math.ceil((authenticatedWithGoogleUntil.getTime() - new Date().getTime()) / 1000);
if (timeLeft > 30) {
return;
}
if (timeLeft <= 0) {
Account.resetGoogleAuthentication();
return;
}
password.placeholder = 'Authenticated! ' + timeLeft + ' seconds left...';
}, 1000);
},
resetGoogleAuthentication: function () {
if (Account.countdown !== null) {
clearInterval(Account.countdown);
}
var password = document.getElementsByTagName('form')[0].elements.password;
var button = document.getElementById('authenticateWithGoogleButton');
password.type = Account.original.type;
password.placeholder = Account.original.placeholder
password.required = Account.original.required;
password.disabled = Account.original.disabled;
button.disabled = false;
}
};
(function () {
document.getElementById('authenticateWithGoogleButton').onclick = function () {
Account.openGoogleAuthenticate();
};
document.getElementsByTagName('form')[0].onreset = function () {
Account.resetGoogleAuthentication();
};
})();

View File

@ -1,10 +0,0 @@
(function () {
if (success) {
window.opener.Account.authenticatedWithGoogleCallback(authenticatedWithGoogleUntil);
window.close();
} else {
document.getElementById('closeWindowButton').onclick = function () {
window.close();
}
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
(function () {
document.getElementById('cancelGoogleSignupButton').onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/google/reset', function () {
window.location.replace('/signup');
});
};
})();

View File

@ -1,12 +0,0 @@
(function () {
var resetSignupButton = document.getElementById('resetSignupButton');
if (resetSignupButton) {
resetSignupButton.onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/signup/reset', function () {
window.location.reload();
});
};
}
})();

View File

@ -1,743 +0,0 @@
'use strict';
(function () {
var MapEditor = {
metadata: {
name: null,
description: null
},
map: null,
panorama: null,
selectedMarker: null,
added: {},
edited: {},
deleted: {},
editMetadata: function () {
var form = document.getElementById('metadataForm');
MapEditor.metadata.name = form.elements.name.value;
MapEditor.metadata.description = form.elements.description.value;
MapEditor.metadata.unlisted = form.elements.unlisted.checked;
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
MapGuesser.hideModal();
document.getElementById('saveButton').disabled = false;
},
getPlace: function (placeId, marker) {
MapGuesser.httpRequest('GET', '/admin/place.json/' + placeId, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (!this.response.panoId) {
document.getElementById('noPano').style.visibility = 'visible';
places[marker.placeId].panoId = -1;
places[marker.placeId].noPano = true;
return;
}
places[marker.placeId].panoId = this.response.panoId;
places[marker.placeId].noPano = false;
MapEditor.loadPano(this.response.panoId, places[marker.placeId].pov);
});
},
loadPano: function (panoId, pov) {
MapEditor.panorama.setVisible(true);
MapEditor.panorama.setPov({ heading: pov.heading, pitch: pov.pitch });
MapEditor.panorama.setZoom(pov.zoom);
MapEditor.panorama.setPano(panoId);
},
loadPanoForNewPlace: function (panoLocationData) {
var placeId = MapEditor.selectedMarker.placeId;
if (!panoLocationData) {
places[placeId].panoId = -1;
places[placeId].noPano = true;
document.getElementById('noPano').style.visibility = 'visible';
return;
}
var latLng = panoLocationData.latLng;
places[placeId].panoId = panoLocationData.pano;
places[placeId].lat = latLng.lat();
places[placeId].lng = latLng.lng();
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
MapEditor.map.panTo(MapEditor.selectedMarker.getLatLng());
MapEditor.panorama.setVisible(true);
MapEditor.panorama.setPov({ heading: 0.0, pitch: 0.0 });
MapEditor.panorama.setZoom(0.0);
MapEditor.panorama.setPano(panoLocationData.pano);
},
requestPanoData: function (location, canBeIndoor) {
var sv = new google.maps.StreetViewService();
sv.getPanorama({
location: location,
preference: google.maps.StreetViewPreference.NEAREST,
radius: MapEditor.map.getSearchRadius(location),
source: canBeIndoor ? google.maps.StreetViewSource.DEFAULT : google.maps.StreetViewSource.OUTDOOR
}, function (data, status) {
var panoLocationData = status === google.maps.StreetViewStatus.OK ? data.location : null;
if (panoLocationData === null && !canBeIndoor) {
MapEditor.requestPanoData(location, true);
return;
}
document.getElementById('loading').style.visibility = 'hidden';
MapEditor.loadPanoForNewPlace(panoLocationData);
});
},
select: function (marker) {
if (MapEditor.selectedMarker === marker) {
MapEditor.closePlace();
return;
}
document.getElementById('map').classList.add('selected');
document.getElementById('control').classList.add('selected');
document.getElementById('noPano').style.visibility = 'hidden';
document.getElementById('panorama').style.visibility = 'visible';
document.getElementById('placeControl').style.visibility = 'visible';
MapEditor.resetSelected();
MapEditor.selectedMarker = marker;
MapEditor.map.resize();
MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false);
if (marker.placeId) {
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
document.getElementById('deleteButton').style.display = 'block';
if (places[marker.placeId].panoId) {
if (places[marker.placeId].panoId === -1) {
document.getElementById('noPano').style.visibility = 'visible';
} else {
MapEditor.loadPano(places[marker.placeId].panoId, places[marker.placeId].pov);
}
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapEditor.getPlace(marker.placeId, marker);
} else {
marker.placeId = 'new_' + new Date().getTime();
var latLng = marker.getLatLng();
places[marker.placeId] = { id: null, lat: latLng.lat, lng: latLng.lng, panoId: null, pov: { heading: 0.0, pitch: 0.0, zoom: 0 }, noPano: false };
document.getElementById('loading').style.visibility = 'visible';
MapEditor.requestPanoData(latLng);
}
},
resetSelected: function (del) {
if (!MapEditor.selectedMarker) {
return;
}
var placeId = MapEditor.selectedMarker.placeId
if (places[placeId].id && !del) {
MapEditor.map.changeMarkerIcon(
MapEditor.selectedMarker,
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
);
} else {
delete places[placeId];
MapEditor.map.removeMarker(MapEditor.selectedMarker);
}
document.getElementById('deleteButton').style.display = 'none';
},
applyPlace: function () {
var placeId = MapEditor.selectedMarker.placeId;
if (!places[placeId].noPano) {
var latLng = MapEditor.panorama.getPosition();
var pov = MapEditor.panorama.getPov();
var zoom = MapEditor.panorama.getZoom();
places[placeId].lat = latLng.lat();
places[placeId].lng = latLng.lng();
places[placeId].panoId = MapEditor.panorama.getPano();
places[placeId].pov = { heading: pov.heading, pitch: pov.pitch, zoom: zoom };
}
if (!places[placeId].id) {
places[placeId].id = placeId;
MapEditor.added[placeId] = places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('deleteButton').style.display = 'block';
} else {
if (!MapEditor.added[placeId]) {
MapEditor.edited[placeId] = places[placeId];
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
} else {
MapEditor.added[placeId] = places[placeId];
}
}
MapEditor.selectedMarker.setLatLng({ lat: places[placeId].lat, lng: places[placeId].lng });
document.getElementById('saveButton').disabled = false;
},
closePlace: function (del) {
document.getElementById('map').classList.remove('selected');
document.getElementById('control').classList.remove('selected');
document.getElementById('noPano').style.visibility = 'hidden';
document.getElementById('panorama').style.visibility = 'hidden';
document.getElementById('placeControl').style.visibility = 'hidden';
MapEditor.resetSelected(del);
MapEditor.selectedMarker = null;
MapEditor.map.resize();
},
deletePlace: function () {
var placeId = MapEditor.selectedMarker.placeId;
if (places[placeId].id && !MapEditor.added[placeId]) {
MapEditor.deleted[placeId] = places[placeId];
document.getElementById('deleted').innerHTML = String(Object.keys(MapEditor.deleted).length);
}
MapEditor.closePlace(true);
delete MapEditor.added[placeId];
delete MapEditor.edited[placeId];
delete places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
document.getElementById('saveButton').disabled = false;
},
saveMap: function () {
document.getElementById('loading').style.visibility = 'visible';
var data = new FormData();
if (MapEditor.metadata.name !== null) {
data.append('name', MapEditor.metadata.name);
}
if (MapEditor.metadata.description !== null) {
data.append('description', MapEditor.metadata.description);
}
if (MapEditor.metadata.unlisted !== null) {
data.append('unlisted', MapEditor.metadata.unlisted);
}
for (var placeId in MapEditor.added) {
if (!MapEditor.added.hasOwnProperty(placeId)) {
continue;
}
data.append('added[]', JSON.stringify(MapEditor.added[placeId]));
}
for (var placeId in MapEditor.edited) {
if (!MapEditor.edited.hasOwnProperty(placeId)) {
continue;
}
data.append('edited[]', JSON.stringify(MapEditor.edited[placeId]));
}
for (var placeId in MapEditor.deleted) {
if (!MapEditor.deleted.hasOwnProperty(placeId)) {
continue;
}
data.append('deleted[]', JSON.stringify(MapEditor.deleted[placeId]));
}
MapGuesser.httpRequest('POST', '/admin/saveMap/' + mapId + '/json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
//TODO: handle this error
return;
}
MapEditor.replacePlaceIdsToReal(this.response.added);
if (mapId === 0) {
mapId = this.response.mapId;
window.history.replaceState(null, '', '/admin/mapEditor/' + mapId);
}
MapEditor.added = {};
MapEditor.edited = {};
MapEditor.deleted = {};
document.getElementById('added').innerHTML = '0';
document.getElementById('edited').innerHTML = '0';
document.getElementById('deleted').innerHTML = '0';
document.getElementById('saveButton').disabled = true;
}, data);
},
replacePlaceIdsToReal: function (addedPlaces) {
for (var i = 0; i < addedPlaces.length; ++i) {
var tempId = addedPlaces[i].tempId;
var placeId = addedPlaces[i].id;
places[tempId].id = placeId;
}
}
};
var Util = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
return { ppi: 320, tileSize: 128, zoomOffset: 1, minZoom: 0, maxZoom: 18 };
} else if (window.devicePixelRatio >= 2) {
return { ppi: 250, tileSize: 256, zoomOffset: 0, minZoom: 1, maxZoom: 19 };
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
}
},
extractCoordinates: function (coordinatesStr) {
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
var delimiters = [',', ' ', ';'];
coordinatesStr = coordinatesStr.trim();
if (coordinatesStr.length == 0) {
return coordinates;
}
for (var delimiter of delimiters) {
if (coordinatesStr.indexOf(delimiter) != -1) {
var coordinatesArr = coordinatesStr.split(delimiter);
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
coordinates.valid = true;
return coordinates;
}
}
}
return coordinates;
}
};
var LMapWrapper = {
map: null,
markers: null,
divId: null,
iconCollection: {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
})
},
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!LMapWrapper.map) {
LMapWrapper.divId = divId;
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2
});
LMapWrapper.map.on('click', function (e) {
LMapWrapper.placeMarker(e.latlng);
});
var highResData = Util.getHighResData();
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: tileSubdomains,
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map);
if (mapId) {
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
}
}
LMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'none';
},
hide: function () {
document.getElementById(LMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!LMapWrapper.markers) {
LMapWrapper.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
} else {
LMapWrapper.markers.clearLayers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
}
LMapWrapper.map.addLayer(LMapWrapper.markers);
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = L.marker(latLng, {
icon: LMapWrapper.iconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
},
panTo: function (latLng) {
LMapWrapper.map.panTo(latLng);
},
resize: function () {
LMapWrapper.map.invalidateSize(true);
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
marker.setZIndexOffset(2000);
},
removeMarker: function (marker) {
LMapWrapper.markers.removeLayer(marker);
},
toggleStreetViewCover: function () { },
getSearchRadius: function (location) {
return 100;
}
};
var GMapWrapper = {
map: null,
markers: null,
divId: null,
streetViewCover: null,
streetViewCoverOn: false,
iconCollection: {
iconGreen: {
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconRed: {
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconBlue: {
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
}
},
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!GMapWrapper.map) {
GMapWrapper.divId = divId;
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
center: { lat: 0., lng: 0. },
zoom: 2,
fullscreenControl: false,
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.LEFT_BOTTOM
},
streetViewControl: false,
draggableCursor: 'crosshair'
});
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
GMapWrapper.placeMarker({
lat: mapsMouseEvent.latLng.lat(),
lng: mapsMouseEvent.latLng.lng()
});
});
if (mapId) {
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
}
}
GMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').style.display = 'block'
},
hide: function () {
document.getElementById(GMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!GMapWrapper.markers) {
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
imagePath: STATIC_ROOT + '/img/markers/m',
imageExtension: 'png?rev' + REVISION
});
} else {
GMapWrapper.markers.clearMarkers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = new google.maps.Marker({
position: {
lat: place.lat,
lng: place.lng
},
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
GMapWrapper.markers.addMarker(marker);
}
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = new google.maps.Marker({
map: GMapWrapper.map,
position: latLng,
icon: GMapWrapper.iconCollection.iconBlue,
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
GMapWrapper.markers.addMarker(marker);
MapEditor.select(marker);
},
panTo: function (latLng) {
GMapWrapper.map.panTo(latLng);
},
resize: function () {
google.maps.event.trigger(GMapWrapper.map, 'resize');
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
},
removeMarker: function (marker) {
GMapWrapper.markers.removeMarker(marker);
},
toggleStreetViewCover: function () {
if (GMapWrapper.streetViewCoverOn) {
GMapWrapper.streetViewCover.setMap(null);
GMapWrapper.streetViewCoverOn = false;
} else {
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
GMapWrapper.streetViewCoverOn = true;
}
},
getSearchRadius: function (location) {
// source: https://www.yorku.ca/mack/CHI01.htm
var movementOffset = 4;
// source: https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ?pli=1
var metersPerPixel = 156543.03392 * Math.cos(location.lat * Math.PI / 180) / Math.pow(2, GMapWrapper.map.getZoom());
var minSearchRadius = 5;
var searchRadius = Math.max(minSearchRadius, Math.round(movementOffset * metersPerPixel));
return searchRadius;
}
};
// initialize content of #map with google maps
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work
fullscreenControl: false,
fullscreenControlOptions: {
position: google.maps.ControlPosition.LEFT_TOP
},
motionTracking: false
});
document.getElementById('mapName').onclick = function (e) {
e.preventDefault();
MapGuesser.showModal('metadata');
document.getElementById('metadataForm').elements.name.select();
};
document.getElementById('metadataForm').onsubmit = function (e) {
e.preventDefault();
MapEditor.editMetadata();
};
document.getElementById('closeMetadataButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('saveButton').onclick = function () {
MapEditor.saveMap();
};
document.getElementById('applyButton').onclick = function () {
MapEditor.applyPlace();
};
document.getElementById('closeButton').onclick = function () {
MapEditor.closePlace();
};
document.getElementById('deleteButton').onclick = function () {
MapEditor.deletePlace();
};
document.getElementById('jumpButton').onclick = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
if (coordinates.valid) {
MapEditor.map.placeMarker(coordinates.latlng);
}
};
document.getElementById('jumpCoordinates').onkeyup = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
var jumpButton = document.getElementById("jumpButton");
if (coordinates.valid) {
jumpButton.disabled = false;
if (e.key == 'Enter') {
MapEditor.map.placeMarker(coordinates.latlng);
}
}
else {
jumpButton.disabled = true;
}
};
document.getElementById('mapSelector').onclick = function () {
MapEditor.closePlace();
MapEditor.map.hide();
if (MapEditor.map === GMapWrapper) {
MapEditor.map = LMapWrapper;
MapEditor.map.init('lmap', places);
} else {
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
}
}
document.getElementById('streetViewCoverSelector').onclick = function () {
MapEditor.map.toggleStreetViewCover();
}
})();

View File

@ -1,281 +0,0 @@
var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false,
sessionAvailableHooks: {},
initGoogleAnalitics: function () {
if (typeof GOOGLE_ANALITICS_ID === 'undefined') {
return;
}
// Global site tag (gtag.js) - Google Analytics
var script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ANALITICS_ID;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GOOGLE_ANALITICS_ID);
},
agreeCookies: function () {
if (MapGuesser.cookiesAgreed) {
return;
}
var expirationDate = new Date(new Date().getTime() + 20 * 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = 'COOKIES_CONSENT=1; expires=' + expirationDate + '; path=/';
MapGuesser.initGoogleAnalitics();
MapGuesser.httpRequest('GET', '/startSession.json', function () {
ANTI_CSRF_TOKEN = this.response.antiCsrfToken;
for (var hookId in MapGuesser.sessionAvailableHooks) {
if (!MapGuesser.sessionAvailableHooks.hasOwnProperty(hookId)) {
continue;
}
MapGuesser.sessionAvailableHooks[hookId]();
}
});
MapGuesser.cookiesAgreed = true;
},
httpRequest: function (method, url, callback, data) {
var xhr = new XMLHttpRequest();
xhr.onload = callback;
xhr.open(method, url, true);
xhr.responseType = 'json';
if (method === 'POST') {
if (typeof data === 'undefined') {
data = new FormData();
}
data.append('anti_csrf_token', ANTI_CSRF_TOKEN);
xhr.send(data);
} else {
xhr.send();
}
},
setOnsubmitForForm: function (form) {
form.onsubmit = function (e) {
e.preventDefault();
document.getElementById('loading').style.visibility = 'visible';
var formData = new FormData(form);
var formError = form.getElementsByClassName('formError')[0];
var pageLeaveOnSuccess = form.dataset.redirectOnSuccess || form.dataset.reloadOnSuccess;
MapGuesser.httpRequest('POST', form.action, function () {
if (!pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
if (this.response.error) {
if (pageLeaveOnSuccess) {
document.getElementById('loading').style.visibility = 'hidden';
}
formError.style.display = 'block';
formError.innerHTML = this.response.error.errorText;
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
return;
}
if (this.response.redirect) {
window.location.replace(this.response.redirect.target);
return;
}
if (!pageLeaveOnSuccess) {
formError.style.display = 'none';
form.reset();
} else {
if (form.dataset.redirectOnSuccess) {
window.location.replace(form.dataset.redirectOnSuccess);
} else if (form.dataset.reloadOnSuccess) {
window.location.reload();
}
}
}, formData);
}
},
showModal: function (id) {
document.getElementById(id).style.visibility = 'visible';
document.getElementById('cover').style.visibility = 'visible';
},
showModalWithContent: function (title, body, extraButtons) {
if (typeof extraButtons === 'undefined') {
extraButtons = [];
}
MapGuesser.showModal('modal');
document.getElementById('modalTitle').textContent = title;
if (typeof body === 'object') {
document.getElementById('modalText').appendChild(body);
} else {
document.getElementById('modalText').textContent = body;
}
var buttons = document.getElementById('modalButtons');
buttons.textContent = '';
for (var i = 0; i < extraButtons.length; i++) {
var extraButton = extraButtons[i];
var button = document.createElement(extraButton.type);
if (extraButton.type === 'a') {
button.classList.add('button');
}
for (var i = 0; i < extraButton.classNames.length; i++) {
button.classList.add(extraButton.classNames[i]);
}
button.classList.add('marginTop');
button.classList.add('marginRight');
button.textContent = extraButton.text;
if (extraButton.type === 'a') {
button.href = extraButton.href;
} else if (extraButton.type === 'button') {
button.onclick = extraButton.onclick;
}
buttons.appendChild(button);
}
var closeButton = document.createElement('button');
closeButton.classList.add('gray');
closeButton.classList.add('marginTop');
closeButton.textContent = 'Close';
closeButton.onclick = function () {
MapGuesser.hideModal();
};
buttons.appendChild(closeButton);
},
hideModal: function () {
var modals = document.getElementsByClassName('modal');
for (var i = 0; i < modals.length; i++) {
modals[i].style.visibility = 'hidden';
}
document.getElementById('cover').style.visibility = 'hidden';
},
observeInput: function (form, observedInputs) {
var anyChanged = false;
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
if (input.type === 'checkbox') {
if (input.defaultChecked !== input.checked) {
anyChanged = true;
}
} else {
if (input.defaultValue !== input.value) {
anyChanged = true;
}
}
}
form.elements['submit_button'].disabled = !anyChanged;
},
observeInputsInForm: function (form, observedInputs) {
for (var i = 0; i < observedInputs.length; i++) {
var input = form.elements[observedInputs[i]];
switch (input.tagName) {
case 'INPUT':
case 'TEXTAREA':
input.oninput = function () {
MapGuesser.observeInput(form, observedInputs);
};
break;
case 'SELECT':
input.onchange = function () {
MapGuesser.observeInput(form, observedInputs);
};
break;
}
}
form.onreset = function () {
form.elements['submit_button'].disabled = true;
}
}
};
(function () {
var anchors = document.getElementsByTagName('a');
for (var i = 0; i < anchors.length; i++) {
var a = anchors[i];
if (a.href !== 'javascript:;' && a.target !== '_blank') {
a.onclick = function () {
document.getElementById('loading').style.visibility = 'visible';
}
}
}
var forms = document.getElementsByTagName('form');
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
if (form.dataset.noSubmit) {
continue;
}
MapGuesser.setOnsubmitForForm(form);
if (form.dataset.observeInputs) {
MapGuesser.observeInputsInForm(form, form.dataset.observeInputs.split(','));
}
}
document.getElementById('cover').onclick = function () {
MapGuesser.hideModal();
};
if (COOKIES_CONSENT) {
MapGuesser.initGoogleAnalitics();
} else {
// we don't want user to agree cookies when clicking on the notice itself
document.getElementById('cookiesNotice').onclick = function (e) {
e.stopPropagation();
};
document.getElementById('agreeCookiesButton').onclick = function () {
MapGuesser.agreeCookies();
document.getElementById('cookiesNotice').style.display = 'none';
};
window.onclick = function () {
MapGuesser.agreeCookies();
};
}
})();

View File

@ -1,181 +0,0 @@
(function () {
var Maps = {
descriptionDivs: null,
addStaticMaps: function () {
var imgContainers = document.getElementById('mapContainer').getElementsByClassName('imgContainer');
for (var i = 0; i < imgContainers.length; i++) {
var imgContainer = imgContainers[i];
var imgSrc = 'https://maps.googleapis.com/maps/api/staticmap?size=350x175&' +
'scale=' + (window.devicePixelRatio >= 2 ? 2 : 1) + '&' +
'visible=' + imgContainer.dataset.boundSouthLat + ',' + imgContainer.dataset.boundWestLng + '|' +
imgContainer.dataset.boundNorthLat + ',' + imgContainer.dataset.boundEastLng +
'&key=' + GOOGLE_MAPS_JS_API_KEY;
imgContainer.style.backgroundImage = 'url("' + imgSrc + '")';
}
},
initializeDescriptionDivs: function () {
Maps.descriptionDivs = document.getElementById('mapContainer').getElementsByClassName('description');
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
description.defaultHeight = boundingClientRect.height;
}
},
calculateDescriptionDivHeights: function () {
var currentY;
var rows = [];
for (var i = 0; i < Maps.descriptionDivs.length; i++) {
var description = Maps.descriptionDivs[i];
var boundingClientRect = description.getBoundingClientRect();
if (currentY !== boundingClientRect.y) {
rows.push([]);
}
rows[rows.length - 1].push(description);
currentY = boundingClientRect.y;
}
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var maxHeight = 0;
for (var j = 0; j < row.length; j++) {
var description = row[j];
if (description.defaultHeight > maxHeight) {
maxHeight = description.defaultHeight;
}
}
for (var j = 0; j < row.length; j++) {
var description = row[j];
description.style.height = maxHeight + 'px';
}
}
}
};
var Util = {
printTimeForHuman: function (time) {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
var time_str = '';
if (minutes == 1) {
time_str += '1 minute';
} else if (minutes > 1) {
time_str += minutes + ' minutes';
}
if (minutes > 0 && seconds > 0) {
time_str += ' and ';
}
if (seconds == 1) {
time_str += '1 second';
} else if (seconds > 1) {
time_str += seconds + ' seconds';
}
return time_str;
}
};
Maps.addStaticMaps();
Maps.initializeDescriptionDivs();
Maps.calculateDescriptionDivHeights();
window.onresize = function () {
Maps.calculateDescriptionDivHeights();
};
document.getElementById('multiForm').onsubmit = function (e) {
e.preventDefault();
var roomId = this.elements.roomId.value;
if (roomId.length !== 6) {
return;
}
window.location.href = '/multiGame/' + this.elements.roomId.value;
};
document.getElementById('challengeForm').onsubmit = function (e) {
e.preventDefault();
var url = '/challenge/create.json';
var formData = new FormData(this);
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', url, function() {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
window.location.href = '/challenge/' + this.response.challengeToken;
}, formData);
};
if (document.getElementById('multiButton')) {
document.getElementById('multiButton').onclick = function () {
MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select();
document.getElementById('playMode').style.visibility = 'hidden';
}
}
if (document.getElementById('challengeButton')) {
document.getElementById('challengeButton').onclick = function () {
MapGuesser.showModal('challenge');
document.getElementById('playMode').style.visibility = 'hidden';
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
};
}
document.getElementById('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeChallengeButton').onclick = function () {
MapGuesser.hideModal();
}
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
document.getElementById('challengeMapId').value = this.dataset.mapId;
};
}
document.getElementById('timeLimit').oninput = function () {
var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
document.getElementById('timerEnabled').checked = true;
}
})();

View File

@ -1,34 +0,0 @@
(function () {
Maps = {
deleteMap: function (mapId, mapName) {
MapGuesser.showModalWithContent('Delete map', 'Are you sure you want to delete map \'' + mapName + '\'?', [{
type: 'button',
classNames: ['red'],
text: 'Delete',
onclick: function () {
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/admin/deleteMap/' + mapId, function () {
if (this.response.error) {
document.getElementById('loading').style.visibility = 'hidden';
//TODO: handle this error
return;
}
window.location.reload();
});
}
}]);
}
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('deleteButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
Maps.deleteMap(this.dataset.mapId, this.dataset.mapName);
};
}
})();

View File

@ -1,6 +0,0 @@
{
"dependencies": {
"leaflet": "^1.6.0",
"leaflet.markercluster": "^1.4.1"
}
}

View File

@ -1,13 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
leaflet.markercluster@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5"
integrity sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==
leaflet@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==

View File

@ -1,15 +0,0 @@
#!/bin/bash
BRANCH_NAME=$(git symbolic-ref --short HEAD)
BRANCH_PATTERN="^(bugfix|feature|hotfix)\/([A-Z]+-[0-9]+).*"
if [[ "${BRANCH_NAME}" =~ $BRANCH_PATTERN ]]; then
TICKET_ID=$(echo $BRANCH_NAME | sed -E "s@$BRANCH_PATTERN@\\2@")
COMMIT_MESSAGE=$(head -n 1 $1)
COMMIT_MESSAGE_REGEX="^$TICKET_ID .*"
if [[ ! "${COMMIT_MESSAGE}" =~ $COMMIT_MESSAGE_REGEX ]]; then
sed -i.bak -e "1s/^/$TICKET_ID /" $1
fi
fi

22
scripts/install.sh Executable file
View File

@ -0,0 +1,22 @@
#!/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 "Minifying JS, CSS and SVG files..."
${ROOT_DIR}/scripts/minify.sh
fi
touch ${ROOT_DIR}/installed

9
scripts/minify.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
uglifyjs ${ROOT_DIR}/public/static/js/game.js -c -m -o ${ROOT_DIR}/public/static/js/game.js
cleancss ${ROOT_DIR}/public/static/css/mapguesser.css -o ${ROOT_DIR}/public/static/css/mapguesser.css
svgo ${ROOT_DIR}/public/static/img/loading.svg -o ${ROOT_DIR}/public/static/img/loading.svg

View File

@ -1,20 +0,0 @@
#!/bin/bash
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <sql_file>"
exit 1
fi
SQL_FILE=$1
ROOT_DIR=$(dirname $(readlink -f "$0"))/..
. ${ROOT_DIR}/.env
if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "This script can only be used in DEV mode!"
exit 1
fi
echo "Running SQL on DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${SQL_FILE}

14
scripts/update.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
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..."
${ROOT_DIR}/scripts/minify.sh
fi

View File

@ -1,51 +0,0 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddUserCommand extends Command
{
public function configure(): void
{
$this->setName('user:add')
->setDescription('Adding of user.')
->addArgument('email', InputArgument::REQUIRED, 'Email of user')
->addArgument('username', InputArgument::REQUIRED, 'Username of user')
->addArgument('password', InputArgument::REQUIRED, 'Password of user')
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');;
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setUsername($input->getArgument('username'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
$user->setType($input->getArgument('type'));
}
try {
\Container::$persistentDataManager->saveToDb($user);
} catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>User was successfully added!</info>');
return 0;
}
}

View File

@ -1,69 +0,0 @@
<?php namespace MapGuesser\Cli;
use FilesystemIterator;
use SokoWeb\View\Linker;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LinkViewCommand extends Command
{
public function configure(): void
{
$this->setName('view:link')
->setDescription('Linking of views.')
->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$views = [];
$view = $input->getArgument('view');
if ($view !== null) {
$views[] = $view;
} else {
$folder = ROOT . '/views';
$folderLength = strlen($folder) + 1;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
continue;
}
$views[] = $view;
}
}
try {
foreach ($views as $view) {
$generator = new Linker($view);
$generator->generate();
}
} catch (\Exception $e) {
$output->writeln('<error>Linking view(s) failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>View(s) successfully linked!</info>');
return 0;
}
}

View File

@ -1,123 +0,0 @@
<?php namespace MapGuesser\Cli;
use DateTime;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MaintainDatabaseCommand extends Command
{
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
public function __construct()
{
parent::__construct();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
}
public function configure(): void
{
$this->setName('db:maintain')
->setDescription('Maintain database.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredRooms();
$this->deleteExpiredSessions();
} catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>Maintenance was successful!</info>');
$output->writeln('');
return 0;
}
private function deleteInactiveExpiredUsers(): void
{
\Container::$dbConnection->startTransaction();
foreach ($this->userRepository->getAllInactiveExpired() as $user) {
//TODO: these can be in some wrapper class
$userConfirmation = $this->userConfirmationRepository->getByUser($user);
if ($userConfirmation !== null) {
\Container::$persistentDataManager->deleteFromDb($userConfirmation);
}
$userPasswordResetter = $this->userPasswordResetterRepository->getByUser($user);
if ($userPasswordResetter !== null) {
\Container::$persistentDataManager->deleteFromDb($userPasswordResetter);
}
foreach ($this->userPlayedPlaceRepository->getAllByUser($user) as $userPlayedPlace) {
\Container::$persistentDataManager->deleteFromDb($userPlayedPlace);
}
\Container::$persistentDataManager->deleteFromDb($user);
}
\Container::$dbConnection->commit();
}
private function deleteExpiredPasswordResetters(): void
{
foreach ($this->userPasswordResetterRepository->getAllExpired() as $passwordResetter) {
\Container::$persistentDataManager->deleteFromDb($passwordResetter);
}
}
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
\Container::$persistentDataManager->deleteFromDb($multiRoom);
}
}
private function deleteExpiredSessions(): void
{
//TODO: model may be used for sessions too
$select = new Select(\Container::$dbConnection, 'sessions');
$select->columns(['id']);
$select->where('updated', '<', (new DateTime('-7 days'))->format('Y-m-d H:i:s'));
$result = $select->execute();
while ($session = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'sessions');
$modify->setId($session['id']);
$modify->delete();
}
}
}

View File

@ -1,131 +0,0 @@
<?php namespace MapGuesser\Cli;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MigrateDatabaseCommand extends Command
{
public function configure(): void
{
$this->setName('db:migrate')
->setDescription('Migration of database changes.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$db = \Container::$dbConnection;
$this->createBaseDb();
$db->startTransaction();
$success = [];
try {
foreach ($this->readDir('structure') as $file) {
$db->multiQuery(file_get_contents($file));
$success[] = $this->saveToDB($file, 'structure');
}
foreach ($this->readDir('data') as $file) {
require $file;
$success[] = $this->saveToDB($file, 'data');
}
} catch (\Exception $e) {
$db->rollback();
$output->writeln('<error>Migration failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$db->commit();
$output->writeln('<info>Migration was successful!</info>');
$output->writeln('');
if (count($success) > 0) {
foreach ($success as $migration) {
$output->writeln($migration);
}
$output->writeln('');
}
return 0;
}
private function createBaseDb()
{
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
AND table_name = \'migrations\';')
->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) {
return;
}
\Container::$dbConnection->multiQuery(file_get_contents(ROOT . '/database/mapguesser.sql'));
}
private function readDir(string $type): array
{
$done = [];
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
$result = $select->execute();
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
$path = ROOT . '/database/migrations/' . $type;
$dir = opendir($path);
if ($dir === false) {
throw new \Exception('Cannot open dir: ' . $path);
}
$files = [];
while ($file = readdir($dir)) {
$filePath = $path . '/' . $file;
if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
continue;
}
$files[] = $filePath;
}
natsort($files);
return $files;
}
private function saveToDB(string $file, string $type): string
{
$baseName = pathinfo($file, PATHINFO_FILENAME);
$modify = new Modify(\Container::$dbConnection, 'migrations');
$modify->set('migration', $baseName);
$modify->set('type', $type);
$modify->save();
return $baseName . ' (' . $type . ')';
}
}

View File

@ -1,298 +1,53 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use MapGuesser\Database\Query\Select;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired; use MapGuesser\Interfaces\Database\IResultSet;
use SokoWeb\Response\HtmlContent; use MapGuesser\Util\Geo\Bounds;
use SokoWeb\Response\JsonContent; use MapGuesser\Response\HtmlContent;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Response\JsonContent;
use SokoWeb\Interfaces\Response\IRedirect; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\UserInChallenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Repository\ChallengeRepository;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use SokoWeb\Response\Redirect;
class GameController implements IAuthenticationRequired class GameController
{ {
const NUMBER_OF_ROUNDS = 5; public function getGame(array $parameters): IContent
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
private ChallengeRepository $challengeRepository;
private UserInChallengeRepository $userInChallengeRepository;
public function __construct()
{ {
$this->multiConnector = new MultiConnector(); $mapId = (int) $parameters['mapId'];
$this->multiRoomRepository = new MultiRoomRepository(); $data = $this->prepareGame($mapId);
$this->mapRepository = new MapRepository(); return new HtmlContent('game', $data);
$this->placeRepository = new PlaceRepository();
$this->challengeRepository = new ChallengeRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
} }
public function isAuthenticationRequired(): bool public function getGameJson(array $parameters): IContent
{ {
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']); $mapId = (int) $parameters['mapId'];
$data = $this->prepareGame($mapId);
return new JsonContent($data);
} }
public function getGame(): IContent private function prepareGame(int $mapId)
{ {
$mapId = (int) \Container::$request->query('mapId'); $bounds = $this->getMapBounds($mapId);
return new HtmlContent('game', ['mapId' => $mapId]); if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $mapId) {
} $_SESSION['state'] = [
public function getNewMultiGame(): IRedirect
{
$mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
$room = new MultiRoom();
$room->setRoomId($roomId);
$room->setStateArray([
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
]);
$room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
}
public function getMultiGame(): IContent
{
$roomId = \Container::$request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function getChallenge(): IContent
{
$challengeToken = \Container::$request->query('challengeToken');
return new HtmlContent('game', ['challengeToken' => $challengeToken]);
}
public function createNewChallenge(): IContent
{
// create Challenge
do {
// initiliaze or if a challenge with the same token already exists
$challengeToken = mt_rand();
} while ($this->challengeRepository->getByToken($challengeToken));
$challenge = new Challenge();
$challenge->setToken($challengeToken);
$challenge->setCreatedDate(new DateTime());
if (\Container::$request->post('timerEnabled') !== null && \Container::$request->post('timeLimit') !== null) {
$challenge->setTimeLimit(\Container::$request->post('timeLimit'));
}
if (\Container::$request->post('timeLimitType') !== null) {
$challenge->setTimeLimitType(\Container::$request->post('timeLimitType'));
}
if (\Container::$request->post('noMove') !== null) {
$challenge->setNoMove(true);
}
if (\Container::$request->post('noPan') !== null) {
$challenge->setNoPan(true);
}
if (\Container::$request->post('noZoom') !== null) {
$challenge->setNoZoom(true);
}
\Container::$persistentDataManager->saveToDb($challenge);
// save owner/creator
$session = \Container::$request->session();
$userId = $session->get('userId');
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
$userInChallenge->setIsOwner(true);
\Container::$persistentDataManager->saveToDb($userInChallenge);
// select places
$mapId = (int) \Container::$request->post('mapId');
// $map = $this->mapRepository->getById($mapId);
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$round = 0;
foreach ($places as $place) {
$placeInChallenge = new PlaceInChallenge();
$placeInChallenge->setPlace($place);
$placeInChallenge->setChallenge($challenge);
$placeInChallenge->setRound($round++);
\Container::$persistentDataManager->saveToDb($placeInChallenge);
}
return new JsonContent(['challengeToken' => dechex($challengeToken)]);
}
public function prepareGame(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId, 'mapId' => $mapId,
'area' => $map->getArea(), 'area' => $bounds->calculateApproximateArea(),
'rounds' => [], 'rounds' => []
'currentRound' => -1 ];
]);
} else { // update the area of the map in the session in any case
$state['area'] = $map->getArea();
$session->set('state', $state);
} }
return new JsonContent([ return ['mapId' => $mapId, 'bounds' => $bounds->toArray()];
'mapId' => $mapId,
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
} }
public function prepareMultiGame(): IContent private function getMapBounds(int $mapId): Bounds
{ {
/** $select = new Select(\Container::$dbConnection, 'maps');
* @var User|null $user $select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
*/ $select->whereId($mapId);
$user = \Container::$request->user();
if ($user === null)
{
return new JsonContent(['error' => 'anonymous_user']);
}
$roomId = \Container::$request->query('roomId'); $map = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$room = $this->multiRoomRepository->getByRoomId($roomId); $bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
if (!isset($room)) { return $bounds;
return new JsonContent(['error' => 'game_not_found']);
}
$state = $room->getStateArray();
$map = $this->mapRepository->getById($state['mapId']);
$token = $this->getMultiToken($roomId);
$members = $room->getMembersArray();
if (!in_array($token, $members['all'])) {
if ($state['currentRound'] >= 0) {
return new JsonContent(['error' => 'game_already_started']);
}
$members['all'][] = $token;
}
$room->setMembersArray($members);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $user->getDisplayName()
]);
return new JsonContent([
'roomId' => $roomId,
'token' => $token,
'owner' => $members['owner'] == $token,
'mapId' => $state['mapId'],
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareChallenge(): IContent
{
$challengeToken_str = \Container::$request->query('challengeToken');
$session = \Container::$request->session();
$userId = $session->get('userId');
if (!isset($userId))
{
return new JsonContent(['error' => 'anonymous_user']);
}
$challenge = $this->challengeRepository->getByTokenStr($challengeToken_str);
if (!isset($challenge))
{
return new JsonContent(['error' => 'game_not_found']);
}
if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) {
// new player is joining
$userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId);
$userInChallenge->setChallenge($challenge);
$userInChallenge->setTimeLeft($challenge->getTimeLimit());
\Container::$persistentDataManager->saveToDb($userInChallenge);
}
$map = $this->mapRepository->getByChallenge($challenge);
return new JsonContent([
'mapId' => $map->getId(),
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
private function getMultiToken(string $roomId): string
{
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
$token = bin2hex(random_bytes(16));
$session->set('multiState', [
'roomId' => $roomId,
'token' => $token
]);
} else {
$token = $multiState['token'];
}
return $token;
} }
} }

View File

@ -1,467 +0,0 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use MapGuesser\Util\Geo\Position;
use SokoWeb\Response\JsonContent;
use SokoWeb\Interfaces\Response\IContent;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\Challenge;
use MapGuesser\PersistentData\Model\Guess;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\Model\PlaceInChallenge;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserPlayedPlace;
use MapGuesser\Repository\GuessRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceInChallengeRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository;
class GameFlowController implements IAuthenticationRequired
{
const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository;
private UserPlayedPlaceRepository $userPlayedPlaceRepository;
private UserInChallengeRepository $userInChallengeRepository;
private PlaceInChallengeRepository $placeInChallengeRepository;
private GuessRepository $guessRepository;
public function __construct()
{
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository();
$this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
$this->userInChallengeRepository = new UserInChallengeRepository();
$this->placeInChallengeRepository = new PlaceInChallengeRepository();
$this->guessRepository = new GuessRepository();
}
public function isAuthenticationRequired(): bool
{
return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
}
public function initialData(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
}
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$this->startNewGame($state, $mapId);
$session->set('state', $state);
}
$response = [];
$last = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $last['panoId'],
'pov' => $last['pov']->toArray()
];
$response['history'] = [];
for ($i = 0; $i < $state['currentRound']; ++$i) {
$round = $state['rounds'][$i];
$response['history'][] = [
'position' => $round['position']->toArray(),
'result' => [
'guessPosition' => $round['guessPosition']->toArray(),
'distance' => $round['distance'],
'score' => $round['score']
]
];
}
return new JsonContent($response);
}
public function multiInitialData(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
}
$places = [];
foreach ($state['rounds'] as $round) {
$places[] = [
'position' => $round['position']->toArray(),
'panoId' => $round['panoId'],
'pov' => $round['pov']->toArray()
];
}
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
return new JsonContent(['ok' => true]);
}
private function prepareChallengeResponse(int $userId, Challenge $challenge, int $currentRound, bool $withHistory = false): array
{
$currentPlace = $this->placeRepository->getByRoundInChallenge($challenge, $currentRound);
// if the last round was played ($currentPlace == null) or history is explicitly requested (for initializing)
if (!isset($currentPlace) || $withHistory) {
$withRelations = ['user', 'place_in_challange', 'place'];
foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) {
$round = $guess->getPlaceInChallenge()->getRound();
if ($guess->getUser()->getId() === $userId) {
$response['history'][$round]['position'] =
$guess->getPlaceInChallenge()->getPlace()->getPosition()->toArray();
$response['history'][$round]['result'] = [
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
} else {
$response['history'][$round]['allResults'][] = [
'userName' => $guess->getUser()->getDisplayName(),
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
}
}
// setting default values for rounds without guesses (because of timeout)
for ($i = 0; $i < $currentRound; ++$i) {
if (!isset($response['history'][$i]) || !isset($response['history'][$i]['result'])) {
$response['history'][$i]['result'] = [
'guessPosition' => null,
'distance' => null,
'score' => 0
];
$response['history'][$i]['position'] =
$this->placeRepository->getByRoundInChallenge($challenge, $i)->getPosition()->toArray();
}
}
$response['history']['length'] = $currentRound;
}
if (!isset($currentPlace)) { // game finished
$response['finished'] = true;
} else { // continue game
$response['place'] = [
'panoId' => $currentPlace->getPanoIdCached(),
'pov' => $currentPlace->getPov()->toArray()
];
$prevRound = $currentRound - 1;
if ($prevRound >= 0) {
foreach ($this->guessRepository->getAllInChallengeByRound($prevRound, $challenge, ['user']) as $guess) {
if ($guess->getUser()->getId() != $userId) {
$response['allResults'][] = [
'userName' => $guess->getUser()->getDisplayName(),
'guessPosition' => $guess->getPosition()->toArray(),
'distance' => $guess->getDistance(),
'score' => $guess->getScore()
];
}
}
}
}
$response['restrictions'] = [
'timeLimit' => $challenge->getTimeLimit() * 1000,
'timeLimitType' => $challenge->getTimeLimitType(),
'noMove' => $challenge->getNoMove(),
'noPan' => $challenge->getNoPan(),
'noZoom' => $challenge->getNoZoom()
];
return $response;
}
public function challengeInitialData(): IContent
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$challengeToken_str = \Container::$request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, ['challenge']);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true);
if ($challenge->getTimeLimitType() === 'game' && $challenge->getTimeLimit() !== null && $userInChallenge->getCurrentRound() > 0) {
$timeLimit = max(10, $userInChallenge->getTimeLeft());
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
return new JsonContent($response);
}
public function guess(): IContent
{
$mapId = (int) \Container::$request->query('mapId');
$session = \Container::$request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$last['guessPosition'] = $guessPosition;
$last['distance'] = $result['distance'];
$last['score'] = $result['score'];
$response = [
'position' => $last['position']->toArray(),
'result' => $result
];
$state['rounds'][$state['currentRound']] = $last;
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$next = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $next['panoId'],
'pov' => $next['pov']->toArray()
];
}
$session->set('state', $state);
$this->saveVisit($last['placeId']);
return new JsonContent($response);
}
// save the selected place for the round in UserPlayedPlace
private function saveVisit($placeId): void
{
$session = \Container::$request->session();
$userId = $session->get('userId');
if (isset($userId)) {
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if (!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId);
} else {
$userPlayedPlace->incrementOccurrences();
}
$userPlayedPlace->setLastTimeDate(new DateTime());
\Container::$persistentDataManager->saveToDb($userPlayedPlace);
}
}
public function multiGuess(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId,
'token' => $multiState['token'],
'guessPosition' => $guessPosition->toArray(),
'distance' => $result['distance'],
'score' => $result['score']
]);
if (isset($responseFromMulti['error'])) {
return new JsonContent(['error' => $responseFromMulti['error']]);
}
$response = [
'position' => $last['position']->toArray(),
'result' => $result,
'allResults' => $responseFromMulti['allResults']
];
return new JsonContent($response);
}
public function challengeGuess(): IContent
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$challengeToken_str = \Container::$request->query('challengeToken');
$userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, ['challenge']);
if (!isset($userInChallenge)) {
return new JsonContent(['error' => 'game_not_found']);
}
$challenge = $userInChallenge->getChallenge();
$currentRound = $userInChallenge->getCurrentRound();
$currentPlaceInChallenge = $this->placeInChallengeRepository->getByRoundInChallenge($currentRound, $challenge, ['place', 'map']);
$currentPlace = $currentPlaceInChallenge->getPlace();
$map = $currentPlace->getMap();
// creating response
$nextRound = $currentRound + 1;
$response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
$response['position'] = $currentPlace->getPosition()->toArray();
if (\Container::$request->post('lat') && \Container::$request->post('lng')) {
$guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
$result = $this->evaluateGuess($currentPlace->getPosition(), $guessPosition, $map->getArea());
// save guess
$guess = new Guess();
$guess->setUserId($userId);
$guess->setPlaceInChallenge($currentPlaceInChallenge);
$guess->setPosition($guessPosition);
$guess->setDistance($result['distance']);
$guess->setScore($result['score']);
\Container::$persistentDataManager->saveToDb($guess);
$response['result'] = $result;
} else {
// user didn't manage to guess in the round in the given timeframe
$response['result'] = ['distance' => null, 'score' => 0];
}
// save user relevant state of challenge
$userInChallenge->setCurrentRound($nextRound);
$timeLeft = \Container::$request->post('timeLeft');
if (isset($timeLeft)) {
$userInChallenge->setTimeLeft(intval($timeLeft));
}
\Container::$persistentDataManager->saveToDb($userInChallenge);
if ($challenge->getTimeLimitType() === 'game' && isset($timeLeft)) {
$timeLimit = max(10, intval($timeLeft));
$response['restrictions']['timeLimit'] = $timeLimit * 1000;
}
if (isset($response['history'][$currentRound]['allResults'])) {
$response['allResults'] = $response['history'][$currentRound]['allResults'];
}
$this->saveVisit($currentPlace->getId());
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = \Container::$request->query('roomId');
$session = \Container::$request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
}
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
\Container::$persistentDataManager->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area)
{
$distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area);
return ['distance' => $distance, 'score' => $score];
}
private function startNewGame(array &$state, int $mapId): void
{
$session = \Container::$request->session();
$userId = $session->get('userId');
$places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
$state['rounds'] = [];
$state['currentRound'] = 0;
foreach ($places as $place) {
$state['rounds'][] = [
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
];
}
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float
{
return $realPosition->calculateDistanceTo($guessPosition);
}
private function calculateScore(float $distance, float $area): int
{
$goodness = 1.0 - ($distance / (sqrt($area) * 1000));
return (int) round(pow(static::MAX_SCORE, $goodness));
}
}

View File

@ -1,21 +1,12 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use SokoWeb\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IRedirect;
use SokoWeb\Interfaces\Response\IRedirect; use MapGuesser\Response\Redirect;
use SokoWeb\Response\JsonContent;
use SokoWeb\Response\Redirect;
class HomeController class HomeController
{ {
public function getIndex(): IRedirect public function getIndex(): IRedirect
{ {
return new Redirect(\Container::$routeCollection->getRoute('maps')->generateLink(), IRedirect::TEMPORARY); return new Redirect([\Container::$routeCollection->getRoute('maps'), []], IRedirect::TEMPORARY);
}
public function startSession(): IContent
{
// session starts with the request, this method just sends valid data to the client
return new JsonContent(['antiCsrfToken' => \Container::$request->session()->get('anti_csrf_token')]);
} }
} }

Some files were not shown because too many files have changed in this diff Show More