151 lines
4.5 KiB
PHP
151 lines
4.5 KiB
PHP
<?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 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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|