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; } }