188 lines
5.4 KiB
PHP
188 lines
5.4 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
|
|
{
|
|
const NUM_DAYS_CUTOFF = 7;
|
|
private AssetIterator $iterator;
|
|
private $layouts;
|
|
private $queue = [];
|
|
|
|
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],
|
|
|
|
// Big-small juxtapositions
|
|
'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
|
'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
|
|
|
// Single row of three
|
|
'landscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
|
'portraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT],
|
|
|
|
// Fallback layouts
|
|
'duo' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
|
'single' => [Image::TYPE_LANDSCAPE | 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()
|
|
{
|
|
// Fetch the first image...
|
|
$image = $this->fetchImage();
|
|
|
|
// No image at all?
|
|
if (!$image)
|
|
return false;
|
|
|
|
// Is it a panorama? Then we've got our row!
|
|
elseif ($image->isPanorama())
|
|
return [[$image], 'panorama'];
|
|
|
|
// Alright, let's initalise a proper row, then.
|
|
$photos = [$image];
|
|
$num_portrait = $image->isPortrait() ? 1 : 0;
|
|
$num_landscape = $image->isLandscape() ? 1 : 0;
|
|
|
|
// Get an initial batch of non-panorama images to work with.
|
|
for ($i = 1; $i < 3 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
|
|
{
|
|
$num_portrait += $image->isPortrait() ? 1 : 0;
|
|
$num_landscape += $image->isLandscape() ? 1 : 0;
|
|
$photos[] = $image;
|
|
}
|
|
|
|
// Sort photos by priority and date captured.
|
|
usort($photos, self::orderPhotos(...));
|
|
|
|
// Three portraits?
|
|
if ($num_portrait === 3)
|
|
return [$photos, 'portraits'];
|
|
|
|
// At least one portrait?
|
|
if ($num_portrait >= 1)
|
|
{
|
|
// Grab two more landscapes, so we can put a total of four tiles on the side.
|
|
for ($i = 0; $image && $i < 2 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
|
|
$photos[] = $image;
|
|
|
|
// We prefer to have the portrait on the side, so prepare to process that first.
|
|
usort($photos, function($a, $b) {
|
|
if ($a->isPortrait() && !$b->isPortrait())
|
|
return -1;
|
|
elseif ($b->isPortrait() && !$a->isPortrait())
|
|
return 1;
|
|
else
|
|
return self::orderPhotos($a, $b);
|
|
});
|
|
|
|
// We might not have a full set of photos, but only bother if we have at least three.
|
|
if (count($photos) > 3)
|
|
return [$photos, 'portrait'];
|
|
}
|
|
|
|
// One landscape at least, hopefully?
|
|
if ($num_landscape >= 1)
|
|
{
|
|
if (count($photos) === 3)
|
|
{
|
|
// We prefer to have the landscape on the side, so prepare to process that first.
|
|
usort($photos, function($a, $b) {
|
|
if ($a->isLandscape() && !$b->isLandscape())
|
|
return -1;
|
|
elseif ($b->isLandscape() && !$a->isLandscape())
|
|
return 1;
|
|
else
|
|
return self::orderPhotos($a, $b);
|
|
});
|
|
|
|
return [$photos, 'landscape'];
|
|
}
|
|
elseif (count($photos) === 2)
|
|
return [$photos, 'duo'];
|
|
else
|
|
return [$photos, 'single'];
|
|
}
|
|
|
|
// Last resort: majority vote
|
|
if ($num_portrait > $num_landscape)
|
|
return [$photos, 'portraits'];
|
|
else
|
|
return [$photos, 'landscapes'];
|
|
}
|
|
|
|
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)
|
|
{
|
|
// Show images of highest priority first.
|
|
$priority_diff = $a->getPriority() - $b->getPriority();
|
|
if ($priority_diff !== 0)
|
|
return -$priority_diff;
|
|
|
|
// In other cases, we'll just show the newest first.
|
|
return $a->getDateCaptured() <=> $b->getDateCaptured();
|
|
}
|
|
|
|
private function pushToQueue(Image $image)
|
|
{
|
|
$this->queue[] = $image;
|
|
}
|
|
}
|