mapguesser/src/Repository/PlaceRepository.php
2021-05-06 17:12:18 +02:00

177 lines
6.4 KiB
PHP

<?php namespace MapGuesser\Repository;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
class PlaceRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $placeId): ?Place
{
return $this->pdm->selectFromDbById($placeId, Place::class);
}
public function getAllForMap(Map $map): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('map_id', '=', $map->getId());
yield from $this->pdm->selectMultipleFromDb($select, Place::class);
}
//TODO: use Map and User instead of id
public function getRandomNPlaces(int $mapId, int $n, ?int $userId): array
{
if(!isset($userId)) { // anonymous single player or multiplayer game
return $this->getRandomNForMapWithValidPano($mapId, $n);
} else { // authorized user
$unvisitedPlaces = $this->getRandomUnvisitedNForMapWithValidPano($mapId, $n, $userId);
$oldPlaces = $this->getRandomOldNForMapWithValidPano($mapId, $n - count($unvisitedPlaces), $userId);
return array_merge($unvisitedPlaces, $oldPlaces);
}
}
//TODO: use Map instead of id
public function getRandomNForMapWithValidPano(int $mapId, int $n, array $exclude = []): array
{
$places = [];
$select = new Select(\Container::$dbConnection, 'places');
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId);
$numberOfPlaces = $select->count();
for ($i = 1; $i <= $n; ++$i) {
$place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude);
$places[] = $place;
$exclude[] = $place->getId();
}
return $places;
}
private function getRandomForMapWithValidPano($numberOfPlaces, $select, array &$exclude, $pickRandomInt = null): ?Place
{
do {
$numberOfPlacesLeft = $numberOfPlaces - count($exclude);
$place = $this->selectRandomFromDbForMap($numberOfPlacesLeft, $select, $exclude, $pickRandomInt);
if($place === null) {
// there is no more never visited place left
return null;
}
$panoId = $place->getFreshPanoId();
if($panoId === null) {
$exclude[] = $place->getId();
}
} while($panoId === null);
return $place;
}
private function selectRandomFromDbForMap($numberOfPlacesLeft, $select, array $exclude, $pickRandomInt): ?Place
{
if($numberOfPlacesLeft <= 0)
return null;
if(!isset($pickRandomInt)) {
$randomOffset = random_int(0, $numberOfPlacesLeft - 1);
} else {
$randomOffset = $pickRandomInt($numberOfPlacesLeft);
}
// $select_unvisited->orderBy('last_time');
$select->where('id', 'NOT IN', $exclude);
$select->limit(1, $randomOffset);
return $this->pdm->selectFromDb($select, Place::class);
}
// Never visited places
private function getRandomUnvisitedNForMapWithValidPano(int $mapId, int $n, int $userId): array
{
$places = [];
$exclude = [];
// list of places visited by user
$selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place');
$selectPlacesByCurrentUser->columns(['place_id', 'last_time']);
$selectPlacesByCurrentUser->where('user_id', '=', $userId);
$selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user');
// count the places never visited
$selectUnvisited = new Select(\Container::$dbConnection, 'places');
$selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectUnvisited->where('map_id', '=', $mapId);
$selectUnvisited->where('last_time', '=', null);
$numberOfUnvisitedPlaces = $selectUnvisited->count();
// look for as many new places as possible but maximum $n
do {
$place = $this->getRandomForMapWithValidPano($numberOfUnvisitedPlaces, $selectUnvisited, $exclude);
if(isset($place)) {
$places[] = $place;
$exclude[] = $place->getId();
}
} while(count($places) < $n && isset($place));
return $places;
}
// Places visited in the longest time
private function getRandomOldNForMapWithValidPano(int $mapId, int $n, int $userId): array
{
$places = [];
$exclude = [];
// list of places visited by user
$selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place');
$selectPlacesByCurrentUser->columns(['place_id', 'last_time']);
$selectPlacesByCurrentUser->where('user_id', '=', $userId);
$selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user');
// count places that were visited at least once
$selectOldPlaces = new Select(\Container::$dbConnection, 'places');
$selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']);
$selectOldPlaces->where('map_id', '=', $mapId);
$numberOfOldPlaces = $selectOldPlaces->count();
// set order by datetime, oldest first
$selectOldPlaces->orderBy('last_time');
// selection algorithm with preference (weighting) for older places using Box-Muller transform
$pickGaussianRandomInt = function($numberOfPlaces) {
$stdev = 0.2;
$avg = 0.0;
$x = mt_rand() / mt_getrandmax();
$y = mt_rand() / mt_getrandmax();
$randomNum = abs(sqrt(-2 * log($x)) * cos(2 * pi() * $y) * $stdev + $avg);
return (int) min($randomNum * $numberOfPlaces, $numberOfPlaces - 1);
};
// look for n - numberOfUnvisitedPlaces places
while(count($places) < $n)
{
$place = $this->getRandomForMapWithValidPano($numberOfOldPlaces, $selectOldPlaces, $exclude, $pickGaussianRandomInt);
if(isset($place))
{
$places[] = $place;
$exclude[] = $place->getId();
}
}
return $places;
}
}