iterator = $iterator; $this->layouts = $this->availableLayouts(); } public function __destruct() { $this->iterator->clean(); } private function availableLayouts() { static $layouts = [ // Single panorama 'panorama' => [Image::TYPE_PANORAMA], // A whopping six landscapes? 'sixLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], // Big-small juxtapositions 'sidePortrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], 'sideLandscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], // Single row of three 'threeLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], 'threePortraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT], // Dual layouts 'dualLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], 'dualPortraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT], // Fallback layouts 'singleLandscape' => [Image::TYPE_LANDSCAPE], 'singlePortrait' => [Image::TYPE_PORTRAIT], ]; return $layouts; } private static function daysApart(Image $a, Image $b) { return $a->getDateCaptured()->diff($b->getDateCaptured())->days; } private function fetchImage($desired_type = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA, Image $refDateImage = null) { // First, check if we have what we're looking for in the queue. foreach ($this->queue as $i => $image) { // Image has to match the desired type and be taken within a week of the reference image. if (self::matchTypeMask($image, $desired_type) && !(isset($refDateImage) && abs(self::daysApart($image, $refDateImage)) > self::NUM_DAYS_CUTOFF)) { unset($this->queue[$i]); return $image; } } // Check whatever's next up! while (($asset = $this->iterator->next()) && ($image = $asset->getImage())) { // Image has to match the desired type and be taken within a week of the reference image. if (self::matchTypeMask($image, $desired_type) && !(isset($refDateImage) && abs(self::daysApart($image, $refDateImage)) > self::NUM_DAYS_CUTOFF)) return $image; else $this->pushToQueue($image); } return false; } public function getRow() { $currentImages = []; $selectedLayout = null; foreach ($this->layouts as $layout => $requiredImageTypes) { // Fetch images per the layout requirements // TODO: pass last image back into fetchImage for date cutoff purposes $currentImages = array_filter(array_map([$this, 'fetchImage'], $requiredImageTypes)); // Matching requirements? if (count($currentImages) === count($requiredImageTypes)) { $selectedLayout = $layout; break; } // Push mismatches back into the queue array_map([$this, 'pushToQueue'], $currentImages); $currentImages = []; } if ($selectedLayout) { // Hurray, we've got something that works usort($currentImages, [$this, 'orderPhotos']); $this->processedImages += count($currentImages); return [$currentImages, $selectedLayout]; } else { // Ensure we have no images left in the iterator before giving up assert($this->processedImages === $this->iterator->num()); return false; } } private static function matchTypeMask(Image $image, $type_mask) { return ($type_mask & Image::TYPE_PANORAMA) && $image->isPanorama() || ($type_mask & Image::TYPE_LANDSCAPE) && $image->isLandscape() || ($type_mask & Image::TYPE_PORTRAIT) && $image->isPortrait(); } private static function orderPhotos(Image $a, Image $b) { // Leave images of different types as-is if ($a->isLandscape() !== $b->isLandscape()) return 0; // Otherwise, show images of highest priority first $priority_diff = $a->getPriority() - $b->getPriority(); return -$priority_diff; } private function pushToQueue(Image $image) { $this->queue[] = $image; } }