forked from Public/pics
Merge pull request 'Rewrite mosaic algorithm using declarative paradigm' (#42) from new-mosaic into master
Reviewed-on: Public/pics#42 Reviewed-by: Roflin <d.brentjes@gmail.com>
This commit is contained in:
commit
f5721c3af7
@ -52,7 +52,7 @@ class ViewPeople extends HTMLController
|
|||||||
'start' => $start,
|
'start' => $start,
|
||||||
'base_url' => BASEURL . '/people/',
|
'base_url' => BASEURL . '/people/',
|
||||||
'page_slug' => 'page/%PAGE%/',
|
'page_slug' => 'page/%PAGE%/',
|
||||||
'index_class' => 'pagination-lg mt-5 justify-content-center',
|
'index_class' => 'pagination-lg mt-5 justify-content-around justify-content-lg-center',
|
||||||
]);
|
]);
|
||||||
$this->page->adopt(new PageIndexWidget($pagination));
|
$this->page->adopt(new PageIndexWidget($pagination));
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class ViewPhotoAlbum extends HTMLController
|
|||||||
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
||||||
'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
|
'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
|
||||||
'page_slug' => 'page/%PAGE%/',
|
'page_slug' => 'page/%PAGE%/',
|
||||||
'index_class' => 'pagination-lg justify-content-center',
|
'index_class' => 'pagination-lg justify-content-around justify-content-lg-center',
|
||||||
]);
|
]);
|
||||||
$this->page->adopt(new PageIndexWidget($index));
|
$this->page->adopt(new PageIndexWidget($index));
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ class ViewTimeline extends HTMLController
|
|||||||
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
|
||||||
'base_url' => BASEURL . '/timeline/',
|
'base_url' => BASEURL . '/timeline/',
|
||||||
'page_slug' => 'page/%PAGE%/',
|
'page_slug' => 'page/%PAGE%/',
|
||||||
'index_class' => 'pagination-lg justify-content-center',
|
'index_class' => 'pagination-lg justify-content-around justify-content-lg-center',
|
||||||
]);
|
]);
|
||||||
$this->page->adopt(new PageIndexWidget($index));
|
$this->page->adopt(new PageIndexWidget($index));
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,17 @@
|
|||||||
|
|
||||||
class PhotoMosaic
|
class PhotoMosaic
|
||||||
{
|
{
|
||||||
|
private AssetIterator $iterator;
|
||||||
|
private $layouts;
|
||||||
|
private $processedImages = 0;
|
||||||
private $queue = [];
|
private $queue = [];
|
||||||
|
|
||||||
const NUM_DAYS_CUTOFF = 7;
|
const NUM_DAYS_CUTOFF = 7;
|
||||||
private AssetIterator $iterator;
|
|
||||||
|
|
||||||
public function __construct(AssetIterator $iterator)
|
public function __construct(AssetIterator $iterator)
|
||||||
{
|
{
|
||||||
$this->iterator = $iterator;
|
$this->iterator = $iterator;
|
||||||
|
$this->layouts = $this->availableLayouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
@ -23,21 +26,40 @@ class PhotoMosaic
|
|||||||
$this->iterator->clean();
|
$this->iterator->clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getRecentPhotos()
|
private function availableLayouts()
|
||||||
{
|
{
|
||||||
return new self(AssetIterator::getByOptions([
|
static $layouts = [
|
||||||
'tag' => 'photo',
|
// Single panorama
|
||||||
'order' => 'date_captured',
|
'panorama' => [Image::TYPE_PANORAMA],
|
||||||
'direction' => 'desc',
|
|
||||||
'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs)
|
// 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 matchTypeMask(Image $image, $type_mask)
|
private static function daysApart(Image $a, Image $b)
|
||||||
{
|
{
|
||||||
return ($type_mask & Image::TYPE_PANORAMA) && $image->isPanorama() ||
|
return $a->getDateCaptured()->diff($b->getDateCaptured())->days;
|
||||||
($type_mask & Image::TYPE_LANDSCAPE) && $image->isLandscape() ||
|
|
||||||
($type_mask & Image::TYPE_PORTRAIT) && $image->isPortrait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fetchImage($desired_type = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA, Image $refDateImage = null)
|
private function fetchImage($desired_type = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA, Image $refDateImage = null)
|
||||||
@ -66,109 +88,63 @@ class PhotoMosaic
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function pushToQueue(Image $image)
|
public function getRow()
|
||||||
{
|
{
|
||||||
$this->queue[] = $image;
|
$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)
|
private static function orderPhotos(Image $a, Image $b)
|
||||||
{
|
{
|
||||||
// Show images of highest priority first.
|
// 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();
|
$priority_diff = $a->getPriority() - $b->getPriority();
|
||||||
if ($priority_diff !== 0)
|
|
||||||
return -$priority_diff;
|
return -$priority_diff;
|
||||||
|
|
||||||
// In other cases, we'll just show the newest first.
|
|
||||||
return $a->getDateCaptured() <=> $b->getDateCaptured();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function daysApart(Image $a, Image $b)
|
private function pushToQueue(Image $image)
|
||||||
{
|
{
|
||||||
return $a->getDateCaptured()->diff($b->getDateCaptured())->days;
|
$this->queue[] = $image;
|
||||||
}
|
|
||||||
|
|
||||||
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'];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,29 +81,17 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.pagination {
|
|
||||||
height: 52px;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.pagination .page-number, .pagination .page-padding {
|
.pagination .page-number, .pagination .page-padding {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.pagination .page-link {
|
.pagination .page-link {
|
||||||
border-radius: var(--bs-pagination-border-radius) !important;
|
border-radius: var(--bs-pagination-border-radius) !important;
|
||||||
}
|
}
|
||||||
.pagination > :first-child, .pagination > :last-child, .pagination .first-wildcard {
|
.pagination > :first-child,
|
||||||
display: inline-block;
|
.pagination > :last-child,
|
||||||
position: absolute;
|
.pagination .first-wildcard,
|
||||||
}
|
.pagination .page-number.active {
|
||||||
.pagination > :first-child {
|
display: inline-flex;
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.pagination > .first-wildcard {
|
|
||||||
left: 48%;
|
|
||||||
}
|
|
||||||
.pagination > :last-child {
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ class PhotosIndex extends Template
|
|||||||
protected $show_edit_buttons;
|
protected $show_edit_buttons;
|
||||||
protected $show_headers;
|
protected $show_headers;
|
||||||
protected $show_labels;
|
protected $show_labels;
|
||||||
protected $row_limit = 1000;
|
|
||||||
protected $previous_header = '';
|
protected $previous_header = '';
|
||||||
protected $url_suffix;
|
protected $url_suffix;
|
||||||
|
|
||||||
@ -47,11 +46,12 @@ class PhotosIndex extends Template
|
|||||||
echo '
|
echo '
|
||||||
<div class="container photo-index">';
|
<div class="container photo-index">';
|
||||||
|
|
||||||
for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--)
|
$i = 0;
|
||||||
|
while ($row = $this->mosaic->getRow())
|
||||||
{
|
{
|
||||||
list($photos, $what) = $row;
|
[$photos, $what] = $row;
|
||||||
$this->header($photos);
|
$this->header($photos);
|
||||||
$this->$what($photos, $i % 2);
|
$this->$what($photos, ($i++) % 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
@ -137,7 +137,14 @@ class PhotosIndex extends Template
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function portrait(array $photos, $altLayout)
|
protected function sixLandscapes(array $photos, $altLayout)
|
||||||
|
{
|
||||||
|
$chunks = array_chunk($photos, 3);
|
||||||
|
$this->sideLandscape($chunks[0], $altLayout);
|
||||||
|
$this->threeLandscapes($chunks[1], $altLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sidePortrait(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
$image = array_shift($photos);
|
$image = array_shift($photos);
|
||||||
|
|
||||||
@ -170,7 +177,7 @@ class PhotosIndex extends Template
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function landscape(array $photos, $altLayout)
|
protected function sideLandscape(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
$image = array_shift($photos);
|
$image = array_shift($photos);
|
||||||
|
|
||||||
@ -203,41 +210,7 @@ class PhotosIndex extends Template
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function duo(array $photos, $altLayout)
|
protected function threeLandscapes(array $photos, $altLayout)
|
||||||
{
|
|
||||||
echo '
|
|
||||||
<div class="row g-5 mb-5 tile-duo">';
|
|
||||||
|
|
||||||
foreach ($photos as $image)
|
|
||||||
{
|
|
||||||
echo '
|
|
||||||
<div class="col-md-6">';
|
|
||||||
|
|
||||||
$this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function single(array $photos, $altLayout)
|
|
||||||
{
|
|
||||||
echo '
|
|
||||||
<div class="row g-5 mb-5 tile-single">
|
|
||||||
<div class="col-md-6">';
|
|
||||||
|
|
||||||
$image = array_shift($photos);
|
|
||||||
$this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
|
||||||
|
|
||||||
echo '
|
|
||||||
</div>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function landscapes(array $photos, $altLayout)
|
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="row g-5 mb-5 tile-row-landscapes">';
|
<div class="row g-5 mb-5 tile-row-landscapes">';
|
||||||
@ -257,7 +230,7 @@ class PhotosIndex extends Template
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function portraits(array $photos, $altLayout)
|
protected function threePortraits(array $photos, $altLayout)
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<div class="row g-5 mb-5 tile-row-portraits">';
|
<div class="row g-5 mb-5 tile-row-portraits">';
|
||||||
@ -277,6 +250,52 @@ class PhotosIndex extends Template
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function dualLandscapes(array $photos, $altLayout)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="row g-5 mb-5 tile-duo">';
|
||||||
|
|
||||||
|
foreach ($photos as $image)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="col-md-6">';
|
||||||
|
|
||||||
|
$this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function dualPortraits(array $photos, $altLayout)
|
||||||
|
{
|
||||||
|
// Recycle the row layout so portraits don't appear too large
|
||||||
|
$this->threePortraits($photos, $altLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function singleLandscape(array $photos, $altLayout)
|
||||||
|
{
|
||||||
|
echo '
|
||||||
|
<div class="row g-5 mb-5 tile-single">
|
||||||
|
<div class="col-md-6">';
|
||||||
|
|
||||||
|
$image = array_shift($photos);
|
||||||
|
$this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
||||||
|
|
||||||
|
echo '
|
||||||
|
</div>
|
||||||
|
</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function singlePortrait(array $photos, $altLayout)
|
||||||
|
{
|
||||||
|
// Recycle the row layout so portraits don't appear too large
|
||||||
|
$this->threePortraits($photos, $altLayout);
|
||||||
|
}
|
||||||
|
|
||||||
public function setUrlSuffix($suffix)
|
public function setUrlSuffix($suffix)
|
||||||
{
|
{
|
||||||
$this->url_suffix = $suffix;
|
$this->url_suffix = $suffix;
|
||||||
|
Loading…
Reference in New Issue
Block a user