Rewrite mosaic algorithm using declarative paradigm #42
@ -8,11 +8,13 @@
|
||||
|
||||
class PhotoMosaic
|
||||
{
|
||||
const NUM_DAYS_CUTOFF = 7;
|
||||
private AssetIterator $iterator;
|
||||
private $layouts;
|
||||
private $processedImages = 0;
|
||||
private $queue = [];
|
||||
|
||||
const NUM_DAYS_CUTOFF = 7;
|
||||
|
||||
public function __construct(AssetIterator $iterator)
|
||||
{
|
||||
$this->iterator = $iterator;
|
||||
@ -31,7 +33,8 @@ class PhotoMosaic
|
||||
'panorama' => [Image::TYPE_PANORAMA],
|
||||
|
||||
// Big-small juxtapositions
|
||||
'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
||||
'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE,
|
||||
Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
||||
'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
|
||||
|
||||
// Single row of three
|
||||
@ -79,87 +82,39 @@ class PhotoMosaic
|
||||
|
||||
public function getRow()
|
||||
{
|
||||
// Fetch the first image...
|
||||
$image = $this->fetchImage();
|
||||
$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));
|
||||
|
||||
// No image at all?
|
||||
if (!$image)
|
||||
// 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;
|
||||
|
||||
// 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)
|
||||
@ -171,13 +126,13 @@ class PhotoMosaic
|
||||
|
||||
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;
|
||||
// Leave images of different types as-is
|
||||
if ($a->isLandscape() !== $b->isLandscape())
|
||||
return 0;
|
||||
|
||||
// In other cases, we'll just show the newest first.
|
||||
return $a->getDateCaptured() <=> $b->getDateCaptured();
|
||||
// Otherwise, show images of highest priority first
|
||||
$priority_diff = $a->getPriority() - $b->getPriority();
|
||||
return -$priority_diff;
|
||||
}
|
||||
|
||||
private function pushToQueue(Image $image)
|
||||
|
Loading…
Reference in New Issue
Block a user