pics/models/PhotoMosaic.php

162 lines
4.8 KiB
PHP
Raw Normal View History

<?php
/*****************************************************************************
* PhotoMosaic.php
* Contains the photo mosaic model, an iterator to create tiled photo galleries.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
class PhotoMosaic
{
private $descending;
private AssetIterator $iterator;
private $layouts;
private $processedImages = 0;
private $queue = [];
const NUM_DAYS_CUTOFF = 7;
public function __construct(AssetIterator $iterator)
{
$this->iterator = $iterator;
$this->layouts = $this->availableLayouts();
$this->descending = $iterator->isDescending();
}
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 orderQueueByDate()
{
usort($this->queue, function($a, $b) {
$score = $a->getDateCaptured() <=> $b->getDateCaptured();
return $score * ($this->descending ? -1 : 1);
});
}
private function pushToQueue(Image $image)
{
$this->queue[] = $image;
$this->orderQueueByDate();
}
}