pepebe
12/4/2016 - 8:14 AM

Dirty hack for moregallery v1.5.6 adding [[+first.view_url]] and [[+last.view_url]] file_urls to simgleImage layouts

Dirty hack for moregallery v1.5.6 adding [[+first.view_url]] and [[+last.view_url]] file_urls to simgleImage layouts

<?php
/**
 * Class mgImage
 */
class mgImage extends xPDOSimpleObject
{
    const MODE_UPLOAD = 'upload';
    const MODE_IMPORT = 'import';
    const MODE_VIDEO = 'video';

    protected $checkedForThumb = false;
    protected $iptcHeaderArray = array ( // thank you, stranger http://php.net/manual/en/function.iptcparse.php#113148
        '2#005'=>'DocumentTitle',
        '2#010'=>'Urgency',
        '2#015'=>'Category',
        '2#020'=>'Subcategories',
        '2#025'=>'Keywords', //added
        '2#040'=>'SpecialInstructions',
        '2#055'=>'CreationDate',
        '2#080'=>'AuthorByline',
        '2#085'=>'AuthorTitle',
        '2#090'=>'City',
        '2#095'=>'State',
        '2#101'=>'Country',
        '2#103'=>'OTR',
        '2#105'=>'Headline',
        '2#110'=>'Source',
        '2#115'=>'PhotoSource',
        '2#116'=>'Copyright',
        '2#120'=>'Caption',
        '2#122'=>'CaptionWriter'
    );

    /**
     * mgImage constructor.
     * @param xPDO|modX $xpdo
     */
    public function __construct(xPDO & $xpdo) {
        parent::__construct($xpdo);

        if (!isset($xpdo->moregallery)) {
            $this->_loadMoreGalleryService();
        }
    }

    public function get($k, $format = null, $formatTemplate= null)
    {
        $value = parent::get($k, $format, $formatTemplate);

        switch ($k)
        {
            case 'width':
            case 'height':
                if ($value < 1) {
                    $resource = $this->getResource();
                    if ($resource && $resource->_getSource()) {
                        $relativeUrl = $resource->getSourceRelativeUrl();
                        $fileName = $this->get('file');
                        if (empty($fileName)) {
                            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moregallery] Image record ' . $this->get('id') . ' on resource ' . $this->get('resource') . ' does not have a file value. This may indicate a corrupt upload. Unable to calculate ' . $k . '.');
                            return 0;
                        }
                        $filePath = $resource->source->getBasePath().$relativeUrl. $fileName;

                        if (file_exists($filePath) && is_file($filePath)) {
                            /**
                             * Using the included Imagine lib we crop the image.
                             */
                            try {
                                /** @var \Imagine\Image\ImagineInterface $imagine */
                                $imagine = $this->xpdo->moregallery->getImagine();

                                // Load the image with imagine and create a resized version
                                $img = $imagine->open($filePath);
                                $size = $img->getSize();

                                $width = $size->getWidth();
                                $height = $size->getHeight();
                                $this->set('width', $width);
                                $this->set('height', $height);
                                if (!$this->isNew()) {
                                    $this->save();
                                }

                                $value = $$k;
                            } catch (Exception $e) {
                                $this->xpdo->log(modX::LOG_LEVEL_ERROR, '[moregallery] Exception ' . get_class($e) . ' fetching size for image record ' . $this->get('id') . ' from path ' . $filePath . ': ' . $e->getMessage());
                            }
                        }
                    }
                }
                break;
        }

        return $value;
    }

    public function getCropsAsArray()
    {
        $array = array();
        $crops = $this->getCrops();

        /** @var mgImageCrop $crop */
        foreach ($crops as $key => $crop)
        {
            $array[$key] = $crop->toArray();
        }

        return $array;
    }

    /**
     * Grabs (and creates) crops for an image.
     *
     * @return mgImageCrop[]
     */
    public function getCrops()
    {
        /**
         * Grab source and relative url info for creating thumbs if necessary
         */
        $resource = $this->getResource();
        $source = false;
        $relativeUrl = '';
        if ($resource && $source = $resource->_getSource()) {
            $relativeUrl = $resource->getSourceRelativeUrl();
        }

        /**
         * Grab the crops definitions so we can prepare a $crops array with the crop objects
         */
        $cropDefinition = $this->xpdo->moregallery->getCrops($resource);
        $crops = array();
        foreach ($cropDefinition as $key => $options)
        {
            $crops[$key] = false;
        }

        /**
         * Grab all crops for this image from the database, and loop over them to add them to $crops
         * but also to get rid of any that isn't used (e.g. after changing the crops setting)
         *
         * @var array $existingCrops
         */
        $existingCrops = $this->xpdo->getCollection('mgImageCrop', array('image' => $this->get('id')));
        /** @var mgImageCrop $cropObject */
        foreach ($existingCrops as $cropObject)
        {
            $cropKey = $cropObject->get('crop');
            if (isset($crops[$cropKey]))
            {
                $url = $cropObject->getThumb($source, $relativeUrl);
                $cropObject->set('thumbnail_url', $url);
                $path = $cropObject->getThumb($source, $relativeUrl, true);
                $cropObject->set('thumbnail_path', $path);
                $crops[$cropKey] = $cropObject;
            }
            else
            {
                $cropObject->remove();
            }
        }

        /**
         * Loop over the collected crops to make sure all mgImageCrop objects have been loaded
         * and create the ones that don't exist yet.
         */
        foreach ($crops as $key => $crop)
        {
            if ($crop === false)
            {
                /** @var mgImageCrop $crop */
                $crop = $this->xpdo->newObject('mgImageCrop');
                $crop->fromArray(
                    array(
                        'image' => $this->get('id'),
                        'crop' => $key,
                    )
                );
                $url = $crop->getThumb($source, $relativeUrl);
                $crop->set('thumbnail_url', $url);
                $path = $crop->getThumb($source, $relativeUrl, true);
                $crop->set('thumbnail_path', $path);
                $crop->save();
                $crops[$key] = $crop;
            }
        }

        return $crops;
    }

    /**
     * @param string $keyPrefix
     * @param bool $rawValues
     * @param bool $excludeLazy
     * @param bool $includeRelated
     *
     * @return array
     */
    public function toArray($keyPrefix= '', $rawValues= false, $excludeLazy= false, $includeRelated= false) {
        $array = parent::toArray($keyPrefix, $rawValues, $excludeLazy, $includeRelated);

        $resource = $this->getResource();
        if (!$rawValues) {

            if ($resource && $resource->_getSource()) {
                // Check if we have a manager thumbnail, and regenerate it if necessary
                if (!$this->checkedForThumb) {
                    $this->checkedForThumb = true;
                    $this->checkManagerThumb();
                }

                $thumb = $this->get('mgr_thumb');
                $relativeUrl = $resource->getSourceRelativeUrl();
                $thumbPath = $resource->source->getBasePath() . $relativeUrl . $thumb;
                $array[$keyPrefix . 'mgr_thumb_path'] = $thumbPath;
                $array[$keyPrefix . 'mgr_thumb'] = $resource->source->getObjectUrl($relativeUrl . $thumb);
                $array[$keyPrefix . 'file_url'] = $resource->source->getObjectUrl($relativeUrl . $array[$keyPrefix . 'file']);
                $array[$keyPrefix . 'file_path'] = $resource->source->getBasePath() . $relativeUrl . $array[$keyPrefix . 'file'];
                $array[$keyPrefix . '_source_is_local'] = ($resource->source->get('class_key') === 'sources.modFileMediaSource');

                $array[$keyPrefix . 'view_url'] = $this->xpdo->makeUrl($resource->get('id'), '', array(
                    $this->xpdo->moregallery->getOption('moregallery.single_image_url_param', null, 'iid') => $this->get('id'),
                ), $this->xpdo->moregallery->getOption('link_tag_scheme', null, 'full'));
            }


            // As of MoreGallery 1.5, all uploaded files already have their EXIF and IPTC data cleansed. However it's
            // possible for older images to break the processor loading of images due to old data. The additional
            // cleaning here ensures that everything works as expected, even if it contains invalid characters.
            $cleanExif = $this->cleanInvalidData($array[$keyPrefix.'exif']);
            $array[$keyPrefix . 'exif'] = $cleanExif;
            $array[$keyPrefix . 'exif_dump'] = print_r($cleanExif, true);
            $array[$keyPrefix . 'exif_json'] = $this->xpdo->toJSON($cleanExif);
            $cleanIptc = $this->cleanInvalidData($array[$keyPrefix.'iptc']);
            $array[$keyPrefix . 'iptc'] = $cleanIptc;
            $array[$keyPrefix . 'iptc_dump'] = print_r($cleanIptc, true);
            $array[$keyPrefix . 'iptc_json'] = $this->xpdo->toJSON($cleanIptc);

            $array[$keyPrefix . 'full_view'] = $this->getManagerEmbed();
        }
        return $array;
    }

    /**
     * @return mgResource|null
     */
    public function getResource() {
        $id = $this->get('resource');
        return $this->xpdo->moregallery->getResource($id);
    }

    /**
     * Removes files along with the image record.
     * 
     * @param array $ancestors
     *
     * @return bool
     */
    public function remove (array $ancestors = array ()) {
        $resource = $this->getResource();
        if ($resource && $resource->_getSource()) {
            $relativeUrl = $resource->getSourceRelativeUrl();

            $mgrThumb = $this->get('mgr_thumb');
            if (!empty($mgrThumb)) {
                $resource->source->removeObject($relativeUrl . $mgrThumb);
            }
            $file = $this->get('file');
            if (!empty($file)) {
                $resource->source->removeObject($relativeUrl . $file);
            }

            $crops = $this->getCrops();
            /** @var mgImageCrop $crop */
            foreach ($crops as $crop) {
                $cropThumbnail = $crop->get('thumbnail');
                if (!empty($cropThumbnail)) {
                    $resource->source->removeObject($relativeUrl . $cropThumbnail);
                }
            }

            if ($resource->source->hasErrors()) {
                $errors = $resource->source->getErrors();
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Error(s) while removing file(s) for mgImage ' . $this->toJSON() . ':' . implode("\n", $errors));
            }
        }


        $this->clearCache();
        
        return parent::remove($ancestors);
    }

    /**
     * @param null $cacheFlag
     *
     * @return bool
     */
    public function save($cacheFlag= null) {
        if ($this->isNew()) {
            $this->setIfEmpty('uploadedon', time());
            $this->setIfEmpty('uploadedby', $this->xpdo->user ? $this->xpdo->user->get('id') : 0);
        }
        $saved = parent::save($cacheFlag);
        $this->clearCache();
        return $saved;
    }

    /**
     * Used by {@see self::save()}, this function only calls $this->set if there is not yet a value for it.
     *
     * @param $key
     * @param $value
     */
    protected function setIfEmpty($key, $value) {
        $current = $this->get($key);
        if (empty($current)) {
            $this->set($key, $value);
        }
    }

    public function clearCache() {
        $cacheOptions = array(xPDO::OPT_CACHE_KEY => 'moregallery');
        $resource = $this->get('resource');

        $this->xpdo->cacheManager->delete('single-image/' . $resource . '/', $cacheOptions);
        $this->xpdo->cacheManager->delete('image-collection/' . $resource . '/', $cacheOptions);
        $this->xpdo->cacheManager->delete('mgimage/'.$resource.'/', $cacheOptions);
        $this->xpdo->cacheManager->delete('mgimages/'.$resource.'/', $cacheOptions);
    }

    /**
     * ppb: Gets the first image in a set.
     *
     * @param string $sortBy
     *
     * @param bool $activeOnly
     * @return null|mgImage
     */
    public function getFirst($sortBy = 'sortorder', $activeOnly = true) {
        $c = $this->xpdo->newQuery('mgImage');
        $c->where(array(
            'resource' => $this->get('resource')
            //,'AND:'.$sortBy.':<' => $this->get($sortBy),
        ));
        if ($activeOnly)
        {
            $c->where(array('active' => true));
        }
        $c->sortby($sortBy, 'ASC');
        $c->limit(1);

        return $this->xpdo->getObject('mgImage', $c);
    }

    /**
     * Gets the image before this one.
     *
     * @param string $sortBy
     *
     * @param bool $activeOnly
     * @return null|mgImage
     */
    public function getPrevious($sortBy = 'sortorder', $activeOnly = true) {
        $c = $this->xpdo->newQuery('mgImage');
        $c->where(array(
            'resource' => $this->get('resource')
            ,'AND:'.$sortBy.':<' => $this->get($sortBy),
        ));
        if ($activeOnly)
        {
            $c->where(array('active' => true));
        }
        $c->sortby($sortBy, 'DESC');
        $c->limit(1);

        return $this->xpdo->getObject('mgImage', $c);
    }

    /**
     * Gets the image after this one.
     *
     * @param string $sortBy
     *
     * @param bool $activeOnly
     * @return null|mgImage
     */
    public function getNext($sortBy = 'sortorder', $activeOnly = true) {
        $c = $this->xpdo->newQuery('mgImage');
        $c->where(array(
             'resource' => $this->get('resource')
            ,'AND:'.$sortBy.':>' => $this->get($sortBy),
        ));
        if ($activeOnly)
        {
            $c->where(array('active' => true));
        }
        $c->sortby($sortBy, 'ASC');
        $c->limit(1);

        return $this->xpdo->getObject('mgImage', $c);
    }

    /**
     * ppb: Gets the last image in a set of images.
     *
     * @param string $sortBy
     *
     * @param bool $activeOnly
     * @return null|mgImage
     */
    public function getLast($sortBy = 'sortorder', $activeOnly = true) {
        $c = $this->xpdo->newQuery('mgImage');
        $c->where(array(
            'resource' => $this->get('resource')
            //,'AND:'.$sortBy.':>' => $this->get($sortBy),
        ));
        if ($activeOnly)
        {
            $c->where(array('active' => true));
        }
        $c->sortby($sortBy, 'DESC');
        $c->limit(1);

        return $this->xpdo->getObject('mgImage', $c);
    }
    
    /**
     * @return array|mixed
     */
    public function getTags() {
        $co = array(xPDO::OPT_CACHE_KEY => 'moregallery');
        $tags = $this->xpdo->cacheManager->get('tags/image/'.$this->get('id'), $co);
        if (is_array($tags)) return $tags;

        $tags = array();
        $c = $this->xpdo->newQuery('mgTag');
        $c->innerJoin('mgImageTag', 'Images');
        $c->where(array(
            'Images.image' => $this->get('id'),
        ));
        /** @var mgTag $tag */
        foreach ($this->xpdo->getIterator('mgTag', $c) as $tag) {
            $tags[] = $tag->toArray();
        }

        $this->xpdo->cacheManager->set('tags/image/' . $this->get('id'), $tags, 0, $co);
        return $tags;
    }

    /**
     * Resize the image to a smaller one for use as mgr_thumb
     *
     * @param $content
     * @param int $width
     * @param int $height
     * @return bool|string
     */
    public function createThumbnail($content, $extension = 'jpg', $width = 250, $height = 250) {
        /** @var \Imagine\Image\ImagineInterface $imagine */
        $imagine = $this->xpdo->moregallery->getImagine();

        // If the image is a PDF file, it needs a bit more work to get it propery parsed.
        if ($extension === 'pdf') {
            $extension = 'png';
            $content = $this->xpdo->moregallery->writePdfAsImageAndReturnContent($content);
        }
        elseif (strtolower($extension) === 'svg') {
            $extension = 'png';
        }

        try {
            $img = $imagine->load($content);
        } catch (Exception $e) {
            $this->xpdo->log(modX::LOG_LEVEL_ERROR, '[moreGallery] Unable to load image for record ' . $this->get('id') . 'to create thumbnail: ' . $e->getMessage());
            return $e->getMessage();
        }

        // Get the size to calculate the way we need to crop this image
        $size = $img->getSize();
        $actualWidth = $size->getWidth();
        $actualHeight = $size->getHeight();

        // Figure out the right size to make sure wide or tall images don't get super blurry
        // Basically this makes sure that the images are at least the defined size, rather than e.g. 250x50.
        if ($actualWidth > $actualHeight) {
            $width = ceil(($actualWidth / $actualHeight) * $width);
        }
        else {
            $height = ceil(($actualHeight / $actualWidth) * $height);
        }

        try {
            // Load the image with imagine and create a resized version
            $thumb = $img->resize(new \Imagine\Image\Box($width, $height));

            // Output the thumbnail as a string
            $options = array(
                'jpeg_quality' => (int)$this->xpdo->moregallery->getOption('moregallery.crop_jpeg_quality', null, '90'),
                'png_compression_level' => (int)$this->xpdo->moregallery->getOption('moregallery.crop_png_compression', null, '9'),
            );

            $thumbContents = $thumb->get($extension, $options);

            // Make the filename the ID, followed by a hash, and the extension (of course).
            $hash = md5(implode('-', $this->get(array('id', 'filename', 'file'))));
            $mgrThumb = $this->get('id') . '_' . $hash . '.' . $extension;

            // Grab the path and create the thumbnail
            $resource = $this->getResource();
            $resource->_getSource();
            $path = $resource->getSourceRelativeUrl();
            $resource->source->createContainer($path . '_thumbs/' , '/');
            $resource->source->errors = array();
            $resource->source->createObject($path . '_thumbs/', $mgrThumb, $thumbContents);
            $this->set('mgr_thumb', '_thumbs/' . $mgrThumb);
        } catch (Exception $e) {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moreGallery] Exception ' . get_class($e) . ' while creating thumbnail: ' . $e->getMessage());
            return $e->getMessage();
        }
        return true;
    }
    
    public function loadExifData($file) {
        $ext = pathinfo($file, PATHINFO_EXTENSION);
        if (in_array(strtolower($ext), array('jpeg', 'jpg', 'tiff'), true) && function_exists('exif_read_data')) {
            try {
                // Fetch EXIF data if we have it.
                $exif = exif_read_data($file, NULL, false, false);
                if (is_array($exif)) {
                    foreach ($exif as $key => $value) {
                        $exif[$key] = $this->cleanInvalidData($value);
                    }
                    $this->set('exif', $exif);
                }
            } catch (Exception $e) {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moreGallery] Exception while trying to read exif data: ' . $e->getMessage());
            }
        } else {
            $this->xpdo->log(xPDO::LOG_LEVEL_WARN, '[moreGallery] This server does not have the exif_read_data function installed. MoreGallery cannot extract exif data now.');
        }
    }

    /**
     * Parses the IPTC data into something a bit more usable
     *
     * @param $data
     * @return array
     */
    public function loadIPTCData($data) {
        $iptc = iptcparse($data);

        $newIptc = array();
        if (is_array($iptc)) {
            foreach ($iptc as $key => $value) {
                if (array_key_exists($key, $this->iptcHeaderArray)) {
                    $key = $this->iptcHeaderArray[$key];
                }

                foreach ($value as &$v) {
                    $v = $this->cleanInvalidData($v);
                }
                unset ($v);

                if (count($value) === 1) {
                    $value = $value[0];
                }

                $newIptc[$key] = $value;
            }

            // Store the cleaned iptc data in the database
            $this->set('iptc', $newIptc);
        }

        return $newIptc;
    }

    /**
     * Prefills the name and tags from the provided IPTC data
     *
     * @param array $iptc
     */
    public function prefillFromIPTC(array $iptc = array())
    {
        $name = '';
        $iptcNameHeaders = array("Caption", "Headline", "DocumentTitle");
        foreach ($iptcNameHeaders as $key) {
            if (isset($iptc[$key]) && !empty($iptc[$key])) {
                $name = $iptc[$key];
            }
        }
        if (!empty($name)) {
            $this->set('name', $name);
        }

        $tags = array();
        $iptcTagHeaders = array('Category', 'Subcategories', 'Keywords');
        foreach ($iptcTagHeaders as $key) {
            if (isset($iptc[$key]) && !empty($iptc[$key])) {
                if (is_array($iptc[$key])) {
                    $tags = array_merge($tags, array_values($iptc[$key]));
                }
                else {
                    $tags[] = $iptc[$key];
                }
            }
        }
        $tags = array_unique($tags);
        if (!empty($tags)) {
            foreach ($tags as $tag) {
                /** @var mgTag $tagObj */
                $tagObj = $this->xpdo->getObject('mgTag', array('display' => $tag));
                if (!$tagObj) {
                    $tagObj = $this->xpdo->newObject('mgTag');
                    $tagObj->fromArray(array(
                        'display' => $tag,
                    ));
                    $tagObj->save();
                }

                /** @var mgImageTag $link */
                $link = $this->xpdo->newObject('mgImageTag');
                $link->fromArray(array(
                    'resource' => $this->get('resource'),
                    'image' => $this->get('id'),
                    'tag' => $tagObj->get('id')
                ));
                $link->save();
            }
        }
    }

    /**
     * @param $content
     * @param $orientation
     * @return bool|string
     */
    public function fixOrientation($content, $orientation, $format)
    {

        try {
            /** @var \Imagine\Image\ImagineInterface $imagine */
            $imagine = $this->xpdo->moregallery->getImagine();

            // Load the image with imagine and create a resized version
            $thumb = $imagine->load($content);

            $degrees = 0;
            switch ($orientation) {
                case 3:
                    $degrees = 180;
                    break;
                case 6:
                    $degrees = 90;
                    break;
                case 8:
                    $degrees = 270;
                    break;
            }
            if ($degrees > 0) {
                $thumb->rotate($degrees);
                $options = array(
                    'jpeg_quality' => (int)$this->xpdo->moregallery->getOption('moregallery.crop_jpeg_quality', null, '90'),
                    'png_compression_level' => (int)$this->xpdo->moregallery->getOption('moregallery.crop_png_compression', null, '9'),
                );
                return $thumb->get($format, $options);
            }
        } catch (Exception $e) {
            $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moreGallery] Exception while creating mgr_thumb: ' . $e->getMessage());
        }
        return false;
    }

    public function copyTo(mgResource $resource, mgResource $oldResource)
    {
        // Get the raw contents of the current image
        $oldImageData = $this->toArray('', true);
        unset($oldImageData['id']);
        
        /** @var mgImage $newImage */
        // Create the new image record
        $newImage = $this->xpdo->newObject('mgImage');
        $newImage->fromArray($oldImageData, '', true);
        $newImage->set('resource', $resource->get('id'));
        $newImage->save();


        // Copy across tags for this image
        $tags = $this->xpdo->getIterator('mgImageTag', array('image' => $this->get('id')));
        foreach ($tags as $tag) {
            /** @var mgImageTag $newTag */
            $newTag = $this->xpdo->newObject('mgImageTag');
            $newTag->fromArray(array(
                'resource' => $resource->get('id'),
                'image' => $newImage->get('id'),
                'tag' => $tag->get('tag'),
            ), '', true);
            $newTag->save();
        }

        // Get the files that need to be copied
        $files = array();
        $files[] = $this->get('file');
        $files[] = $this->get('mgr_thumb');

        // Get the crops for this image and copy those, also add the cropped thumbnails to the $files array
        $crops = $this->getCrops();
        foreach ($crops as $crop) {
            /** @var mgImageCrop $crop */
            $newCrop = $this->xpdo->newObject('mgImageCrop');
            $newCrop->fromArray($crop->toArray());
            $newCrop->set('image', $newImage->get('id'));
            $newCrop->save();
            $files[] = $crop->get('thumbnail');
        }

        $this->xpdo->moregallery->setResource($oldResource);
        $relativeUrl = $oldResource->getSourceRelativeUrl();
        $oldBasePath = $oldResource->_getSource()->getBasePath() . $relativeUrl;

        $this->xpdo->moregallery->setResource($resource);
        $relativeUrl = $resource->getSourceRelativeUrl();
        $newBasePath = $resource->_getSource()->getBasePath() . $relativeUrl;

        if (!is_dir($newBasePath)) {
            $resource->source->createContainer($relativeUrl, '');
            $resource->source->createContainer($relativeUrl . '_thumbs/', '');
        }

        foreach ($files as $file) {
            if (!file_exists($oldBasePath . $file)) {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moreGallery] Error copying file ' . $file . ' from ' . $oldBasePath . ' to ' . $newBasePath . ' while trying to duplicate image ' . $this->get('id') . ' because the source image does not exist.');
            }
            elseif (!copy($oldBasePath . $file, $newBasePath . $file)) {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moreGallery] Error copying file ' . $file . ' from ' . $oldBasePath . ' to ' . $newBasePath . ' while trying to duplicate image ' . $this->get('id'));
            }
        }

        return $newImage;
    }

    private function _loadMoreGalleryService()
    {
        $corePath = $this->xpdo->getOption('moregallery.core_path', null, $this->xpdo->getOption('core_path') . 'components/moregallery/');
        $moreGallery = $this->xpdo->getService('moregallery', 'moreGallery', $corePath . 'model/moregallery/');
        if (!($moreGallery instanceof moreGallery)) {
            $this->xpdo->log(modX::LOG_LEVEL_ERROR, 'Error loading moreGallery class from ' . $corePath, '', __METHOD__, __FILE__, __LINE__);
        }
    }

    /**
     * @param $value
     * @return string
     */
    public function cleanInvalidData($value)
    {
        if (is_array($value)) {
            foreach ($value as $key => $subValue) {
                $value[$key] = $this->cleanInvalidData($subValue);
            }
            return $value;
        }
        else {
            return preg_replace('/[^\PC\s]/u', '', $value);
        }
    }

    /**
     * Gets an extended property on the object. Use this instead of interacting with the `properties` value directly.
     *
     * @param $key
     * @param null $default
     * @return null
     */
    public function getProperty($key, $default = null)
    {
        $properties = $this->getProperties();
        if (isset($properties[$key])) {
            return $properties[$key];
        }
        return $default;
    }

    /**
     * Returns all saved properties
     *
     * @return array
     */
    public function getProperties()
    {
        $properties = $this->get('properties');
        if (!is_array($properties)) {
            $properties = array();
        }
        return $properties;
    }

    /**
     * Sets an extended property on the object. Use this instead of interacting with the `properties` value directly.
     *
     * @param string $key
     * @param null $value
     */
    public function setProperty($key, $value = null)
    {
        $properties = $this->getProperties();
        if (!is_array($properties)) {
            $properties = array();
        }
        $properties[$key] = $value;
        $this->setProperties($properties);
    }

    /**
     * Sets an array of properties on the object. If $merge is true, it will do an array_merge with the current data first
     * and otherwise it will overwrite it completely.
     *
     * @param $properties
     * @param bool $merge
     */
    public function setProperties($properties, $merge = true)
    {
        if ($merge) {
            $properties = array_merge($this->getProperties(), $properties);
        }
        $this->set('properties', $properties);
    }

    /**
     * Unsets the property specified by $key, and returns its value (if any).
     *
     * @param $key
     * @return mixed
     */
    public function unsetProperty($key)
    {
        $properties = $this->getProperties();
        if (isset($properties[$key])) {
            $value = $properties[$key];
            unset($properties[$key]);
            $this->setProperties($properties, false);
            return $value;
        }
        return null;
    }

    /**
     * Unsets all properties defined in $keys. Returns an array of $key => $oldValue values.
     *
     * @param array $keys
     * @return array
     */
    public function unsetProperties(array $keys = array())
    {
        $values = array();
        foreach ($keys as $key) {
            $values[$key] = $this->unsetProperty($key);
        }
        return $values;
    }

    /**
     * Returns a HTML embed code for the video.
     *
     * @return string
     */
    public function getManagerEmbed() {
        $resource = $this->getResource();
        if (!$resource || !$resource->_getSource()) {
            return '';
        }
        $relativeUrl = $resource->getSourceRelativeUrl();
        $file = $this->get('file');
        $fileUrl = $resource->source->getBaseUrl() . $relativeUrl . $file;
        
        $extension = pathinfo($file, PATHINFO_EXTENSION);
        if (strtolower($extension) === 'pdf') {
            return <<<HTML
            <object width="100%" height="500" type="application/pdf" data="$fileUrl">
            <p>Unable to preview PDF file.</p>
</object>
HTML;
        }
        
        return '<img src="' . $fileUrl . '">';
    }

    /**
     * Checks if the manager thumb is available, and creates it if not.
     *
     * @return bool
     */
    public function checkManagerThumb()
    {
        $resource = $this->getResource();
        if (!$resource || !$resource->_getSource()) {
            return false;
        }

        $relativeUrl = $resource->getSourceRelativeUrl();
        $thumb = $this->get('mgr_thumb');
        $thumbPath = $resource->source->getBasePath() . $relativeUrl . $thumb;
        if (empty($thumb) || !file_exists($thumbPath)) {
            $resource->source->errors = array();
            $file = $this->get('file');
            if (empty($file)) {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, '[moregallery] Image record ' . $this->get('id') . ' on resource ' . $this->get('resource') . ' does not have a file value. This may indicate a corrupt upload. Unable to create manager thumbnail.');
                return false;
            }
            $content = $resource->source->getObjectContents($relativeUrl . $file);
            if (!$resource->source->hasErrors()) {
                $extension = pathinfo($this->get('file'), PATHINFO_EXTENSION);
                if ($this->createThumbnail($content['content'], $extension)) {
                    return $this->save();
                }
            }
            else {
                $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Error(s) loading file ' . $relativeUrl . $this->get('file') . ' to regenerate thumbnail for image ' . $this->get('id') . ' in gallery ' . $this->get('resource') . ' from media source ' . $resource->source->get('id') . ' : ' . print_r($resource->source->getErrors(), true));

            }
        }
        return false;
    }
}
<div  class="item">

    <div class="row">
        <div id="item_[[+resource.id]]">
            <div class="col-lg-1 col-md-1"></div>
            <div class="col-lg-4 col-md-5 left">
                  	    
                <a href="[[+next.view_url:default=`[[+first.view_url]]`]]" class="item_image small small_[[+custom.class]]">
                    [[pthumb? 
                        &input=`[[+file_url]]` 
                        &options=`w=[[+custom.class:is=`hoch`:then=`400`:else=`500`]]` 
                        &toPlaceholder=`thumb`
                    ]]                    
                    <img 
                        src="[[+thumb]]"
                        width="[[+thumb.width]]" 
                        height="[[+thumb.height]]"
                        alt="[[+name]]"                        
                    />
                </a>                    
            </div>
            <div class="col-lg-1 col-md-1"></div>
            <div class="col-lg-6 col-ms-5 right">
                
                <div class="item_caption" style="height: [[+thumb.height]]px">
                    <div class="item_details">

                        [[+idx:is=`0`:then=`
                        <p class="item_description">
                            [[+resource.longtitle]], [[#[[+resource.parent]].pagetitle]]<br/>
                            [[+resource.description:nl2br]]
                        </p>
                        <p class="image_description">
                    	    [[+description:notempty=`[[+total:gt=`1`:then=`[[+description:replace=`Ausstellungsansicht==Ausstellungsansichten`:nl2br]]`:else=`[[+description:nl2br]]`]]<br/>`]]
                    	    Foto[[+total:gt=`1`:then=`s`:else=``]]: [[+custom.fotograf]]
                    	</p>                        
                        `:else=`
                        <p class="image_description">
                    	    [[+description:nl2br]]
                    	</p>
                        `]]
                        <p>
                    	    <span>Abbildung [[+idx]] / [[+image_count]]</span>
                    	    <br/>
        
                    	    <a href="[[+prev.view_url:default=`[[+last.view_url]]`]]">vorherige Abbildung</a>
                            <br>
                    	    <a href="[[+next.view_url:default=`[[+first.view_url]]`]]">nächste Abbildung</a>
                    	    <br>
                    	    <a href="[[~[[+resource.id]]]]">weitere Abbildungen (Übersicht)</a>


                	    [[+idx:is=`0`:then=`
                	        <br/>
                    	    <a href="[[~[[+resource.parent]]]]?back=[[*id]]&details=0#item_[[+resource.id]]">
                    	       zurück zu Bildarchiv &ndash; [[#[[+resource.parent]].pagetitle]]
                    	    </a>

                	    `:else=``]]
                	        <br/>
                	        <a href="[[+file_url]]">
                	            Download ([[+width]] x [[+height]] px)
                	        </a>
                        </p>      
                        
                        <p>
                            Test: [[+xyz]]
                        </p>
                    </div>
                    <!-- Backlings, etc -->
                </div>
            </div>
        </div>
        <!-- #item[[+resource.id]] -->
    </div>
</div>
<?php
use modmore\Alpacka\Snippet;
use modmore\Alpacka\Properties\SimpleProperty;
use modmore\Alpacka\Properties\BooleanProperty;
use modmore\Alpacka\Properties\EnumProperty;

/**
 * Class GetImagesSnippet
 *
 * Gets a collection of images and videos.
 *
 * @property moreGallery $service
 */
class GetImages extends Snippet {
    protected $resourceFields = array(
        'id', 'alias', 'uri', 'uri_override',
        'pagetitle', 'longtitle', 'menutitle',
        'description', 'introtext', 'link_attributes',
        'parent', 'context_key', 'template', 'class_key', 'content_type',
        'published', 'pub_date', 'unpub_date', 'publishedon', 'publishedby',
        'isfolder', 'richtext', 'searchable', 'cacheable', 'deleted', 'hide_children_in_tree', 'show_in_tree',
        'createdby', 'createdon', 'editedby', 'editedon', 'deletedby', 'deletedon',
    );
    protected $cacheOptions = array(
        xPDO::OPT_CACHE_KEY => 'moregallery'
    );

    /** @var modResource[] */
    protected $_resources = array();
    /** @var array[] */
    protected $_resourceData = array();
    protected $chunkHash = '';
    protected $_idx = 0;
    protected $singleImageParam = '';

    /**
     * The available properties for this snippet.
     *
     * @return array
     */
    public function getPropertiesDefinition()
    {
        return array(
            'cache' => new BooleanProperty(true),
            'resource' => new SimpleProperty(0),
            'activeOnly' => new BooleanProperty(true),
            'sortBy' => new SimpleProperty('sortorder'),
            'sortDir' => new EnumProperty('ASC', array('ASC', 'DESC')),
            'where' => new SimpleProperty(),

            'tags' => new SimpleProperty(),
            'tagsFromUrl' => new SimpleProperty(),
            'tagSeparator' => new SimpleProperty("\n"),

            'getTags' => new BooleanProperty(true),
            'getResourceContent' => new BooleanProperty(false),
            'getResourceProperties' => new BooleanProperty(false),
            'getResourceFields' => new BooleanProperty(false),
            'getResourceTVs' => new SimpleProperty(),

            'tagTpl' => new SimpleProperty('mgtag'),
            'imageTpl' => new SimpleProperty('mgimage'),
            'youtubeTpl' => new SimpleProperty('mgYoutube'),
            'vimeoTpl' => new SimpleProperty('mgVimeo'),
            'imageSeparator' => new SimpleProperty("\n"),
            'singleImageTpl' => new SimpleProperty('mgimagesingle'),
            'singleYoutubeTpl' => new SimpleProperty('mgYoutubeSingle'),
            'singleVimeoTpl' => new SimpleProperty('mgVimeoSingle'),
            'singleImageEnabled' => new BooleanProperty(true),
            'singleImageParam' => new SimpleProperty(),
            'singleImageResource' => new SimpleProperty(0),
            'wrapperTpl' => new SimpleProperty(),
            'wrapperIfEmpty' => new BooleanProperty(true),
            'toPlaceholder' => new SimpleProperty(),

            'limit' => new SimpleProperty(0),
            'offset' => new SimpleProperty(0),
            'totalVar' => new SimpleProperty('total'),

            'scheme' => new SimpleProperty($this->service->getOption('link_tag_scheme')),

            'debug' => new BooleanProperty(false),
            'timing' => new BooleanProperty(false),
        );
    }

    public function initialize()
    {
        $resourceId = $this->getProperty('resource');
        if ($resourceId < 1 && $this->modx->resource) {
            $resourceId = $this->modx->resource->get('id');
            $this->setProperty('resource', $resourceId);
        }

        // Get the singleImageParam property and default it to the setting if empty
        $singleImageParam = $this->getProperty('singleImageParam');
        if (empty($singleImageParam)) {
            $singleImageParam = $this->service->getOption('moregallery.single_image_url_param');
        }
        $this->singleImageParam = $singleImageParam;

        $context = 'web';
        if ($resourceId > 0) {
            if ($this->modx->resource && $this->modx->resource->get('id') == $resourceId) {
                $context = $this->modx->resource->get('context_key');
            }
            else {
                $resource = $this->modx->getObject('modResource', (int)$resourceId);
                if ($resource instanceof modResource) {
                    $context = $resource->get('context_key');
                }
            }
        }
        $this->debug('Setting working context to ' . $context);
        $this->service->setWorkingContext($context);
    }

    /**
     * @return string
     */
    public function process()
    {
        $this->initialize();

        if (array_key_exists($this->singleImageParam, $_REQUEST)) {
            $this->debug('URL parameter "' . $this->singleImageParam . '" exists on $_REQUEST, so showing single image.');
            return $this->getSingleImage((int)$_REQUEST[$this->singleImageParam]);
        }
        $this->debug('No URL parameter ' . $this->singleImageParam . ' exists on $_REQUEST, so showing image collection.');
        return $this->getImageCollection();
    }

    /**
     * Returns image $imageId, from cache if possible.
     *
     * @param $imageId
     * @return string
     * @throws \modmore\Alpacka\Exceptions\InvalidPropertyException
     */
    public function getSingleImage($imageId)
    {
        // Try to load from cache first
        $cacheKey = 'single-image/' . $this->getProperty('resource') . '/' . $imageId . '_' . $this->getPropertyHash() . '_' . $this->getChunkHash();
        if ($this->getProperty('cache')) {
            $cached = $this->modx->cacheManager->get($cacheKey, $this->cacheOptions);
            if (is_array($cached)) {
                $this->debug('Loaded single image ' . $imageId . ' from cache using cacheKey ' . $cacheKey);
                return $this->finish($cached['formatted']);
            }
        }

        // No cache available, so fetch it from the database.
        $filter = array(
            'id' => $imageId,
            'resource' => $this->getProperty('resource'),
        );
        if ($this->getProperty('activeOnly')) {
            $filter['active'] = true;
        }
        /** @var mgImage $image */
        $image = $this->modx->getObject('mgImage', $filter);

        // If the image can't be found, we send the user to the error page.
        // @todo Consider redirecting the user to the "parent" page (i.e. current page without &iid url param) instead?
        if (!$image) {
            $this->debug('Image not found with filter' . print_r($filter, true));
            $this->modx->sendErrorPage();
        }

        $this->debug('Image loaded, now loading all placeholders.');
        // Turn the image into placeholders, including previous and next images.
        $phs = $this->getImagePlaceholders($image, true);

        // Format it
        $tpl = $this->determineSingleTpl($phs);
        $this->debug('Formatting image with chunk ' . $tpl);
        $formatted = $this->service->getChunk($tpl, $phs);

        // If cache is enabled, write the data to the proper cache file.
        if ($this->getProperty('cache')) {
            $cached = array(
                'placeholders' => $phs,
                'formatted' => $formatted,
            );
            $this->debug('Caching image information and formatted output to cacheKey ' . $cacheKey);
            $this->modx->cacheManager->set($cacheKey, $cached, 0, $this->cacheOptions);
        }

        return $this->finish($formatted);
    }

    /**
     *
     *
     * @return string
     * @throws \modmore\Alpacka\Exceptions\InvalidPropertyException
     */
    public function getImageCollection()
    {
        // Set the tags propery to include tags from the URL, so that the cacheKey is updated when tags change
        $this->setProperty('tags', $this->getTags());
        $random = in_array(strtolower($this->getProperty('sortBy')), array('random', 'rand', 'rand()'), true);
        $limit = $this->getProperty('limit');


        // Try to load from cache
        $cacheKey = 'image-collection/' . $this->getProperty('resource') . '/' . $this->getPropertyHash() . '_' . $this->getChunkHash();
        if ($this->getProperty('cache')) {
            $cached = $this->modx->cacheManager->get($cacheKey, $this->cacheOptions);
            if (is_array($cached)) {
                $this->debug('Loaded image collection from cache using cacheKey ' . $cacheKey);

                $formatted = $cached['formatted'];

                if ($random && array_key_exists('result_set', $cached)) {
                    $this->debug('Randomising and parsing result set from cache.');
                    shuffle($cached['result_set']);

                    if ($limit > 0) {
                        $this->debug('Limiting result set to ' . $limit);
                        $cached['result_set'] = array_slice($cached['result_set'], 0, $limit);
                    }

                    $formatted = $this->parseCollection($cached['result_set'], $cached['total']);
                }

                return $this->finish($formatted);
            }
        }

        $this->debug('Preparing SQL query to retrieve images and related data.');

        $c = $this->modx->newQuery('mgImage');
        $c->distinct(true);

        $resource = $this->getProperty('resource');
        if (strpos($resource, ',') !== false) {
            $c->where(array(
                'resource:IN' => explode(',', $resource),
            ));
        }
        else {
            $c->where(array(
                'resource' => $resource,
            ));
        }

        if ($this->getProperty('activeOnly')) {
            $c->where(array(
                'active' => true,
            ));
        }

        $customCondition = $this->getProperty('where');
        if (!empty($customCondition)) {
            $customConditionArray = json_decode($customCondition, true);
            if (is_array($customConditionArray)) {
                $c->where($customConditionArray);
            }
            else {
                $this->debug('WARNING: Custom condition ' . $customCondition . ' is not valid JSON.');
            }
        }

        $this->addTagsCondition($c);

        // Get the total and make it available for getPage support
        $this->debug('Fetching total count for query');
        $total = $this->modx->getCount('mgImage', $c);
        $this->debug('Total results: ' . $total);
        $this->modx->setPlaceholder($this->getProperty('totalVar'), $total);

        // Apply the limit if we're not using a random sort
        if (!$random && $limit > 0) {
            $c->limit($limit, $this->getProperty('offset'));
        }

        // Apply sorting if this isn't a random sort
        if (!$random) {
            $c->sortby($this->getProperty('sortBy'), $this->getProperty('sortDir'));
        }
        else {
            $c->sortby('RAND()');
        }

        if ($this->getProperty('debug')) {
            $c->prepare();
            $this->debug('Executing query: ' . $c->toSQL());
        }

        // Loop over the images and put them into $data
        $data = array();
        $i = 0;
        $this->debug('Iterating over images');
        /** @var mgImage[] $iterator */
        $iterator = $this->modx->getIterator('mgImage', $c);
        
        $lastIndex = count($iterator) -1;
        
        foreach ($iterator as $image) {

            $phs = $this->getImagePlaceholders($image);
            $data[$i] = $phs;

            // Add prev and next options
            if (isset($data[$i - 1])) {
                $data[$i]['prev'] = $data[$i - 1];
                $data[$i - 1]['next'] = $phs;
                // Prevent some sort of recursive array
                unset($data[$i]['prev']['prev']);
            }
            $i++;
        }
        
        // If we're dealing with random images, the limit wasn't applied to the SQL query.
        // So we splice it here.
        $fullResultSet = $data;
        if ($random && $limit > 0) {
            $this->debug('Splicing result set for random sort');
            $data = array_splice($data, 0, $limit);
        }

        $this->debug('Parsing image collection');
        // Loop over the items and parse them through the imageTpl
        $formatted = $this->parseCollection($data, $total);

        // If cache is enabled, write the data to the proper cache file.
        if ($this->getProperty('cache')) {
            $this->debug('Caching formatted and raw results to ' . $cacheKey);
            $cached = array(
                'total' => $total,
                'formatted' => $formatted,
                'result_set' => $fullResultSet,
            );
            $this->modx->cacheManager->set($cacheKey, $cached, 0, $this->cacheOptions);
        }

        return $this->finish($formatted);
    }


    /**
     * Turns an image object into placeholders, including loading related data (crops, tags, url, resource stuff).
     *
     * @param mgImage $image
     * @return array
     */
    public function getImagePlaceholders(mgImage $image, $previousAndNext = false)
    {
        // Start collecting all placeholders with just the standard image info.
        $phs = $image->toArray();

        $phs['idx'] = $this->_idx++;

        // Get the crops for the image
        $phs['crops'] = $image->getCropsAsArray();

        // Process the url placeholder into a link tag
        if (is_numeric($phs['url']) && $phs['url'] > 0) {
            $phs['url'] = '[[~' . $phs['url'] . ']]';
        }

        // Generate a view_url placeholder for a detail page
        $singleImageResource = $this->getProperty('singleImageResource');
        if ($singleImageResource < 1) {
            $singleImageResource = $phs['resource'];
        }
        $phs['view_url'] = $this->modx->makeUrl($singleImageResource, '', array(
            $this->singleImageParam => $image->get('id'),
        ), $this->getProperty('scheme'));

        // If requested, load all the resource fields
        if ($this->getProperty('getResourceFields')) {
            $phs = array_merge($phs,  $this->getResourceFields($image->get('resource')));
        }

        if ($previousAndNext) {

            $first = $image->getFirst($this->getProperty('sortBy'), $this->getProperty('activeOnly'));
            if ($first instanceof mgImage) {
                $phs['first'] = $first->toArray();
                if (is_numeric($phs['first']['url']) && $phs['first']['url'] > 0) {
                    $phs['first']['url'] = '[[~' . $phs['first']['url'] . ']]';
                }
                $phs['first']['view_url'] = $this->modx->makeUrl($singleImageResource, '', array(
                    $this->singleImageParam => $first->get('id'),
                ), $this->getProperty('scheme'));
                $phs['first']['crops'] = $first->getCropsAsArray();
            }
            
            $previous = $image->getPrevious($this->getProperty('sortBy'), $this->getProperty('activeOnly'));
            if ($previous instanceof mgImage) {
                $phs['prev'] = $previous->toArray();
                if (is_numeric($phs['prev']['url']) && $phs['prev']['url'] > 0) {
                    $phs['prev']['url'] = '[[~' . $phs['prev']['url'] . ']]';
                }
                $phs['prev']['view_url'] = $this->modx->makeUrl($singleImageResource, '', array(
                    $this->singleImageParam => $previous->get('id'),
                ), $this->getProperty('scheme'));
                $phs['prev']['crops'] = $previous->getCropsAsArray();
            }

            $next = $image->getNext($this->getProperty('sortBy'), $this->getProperty('activeOnly'));
            if ($next instanceof mgImage) {
                $phs['next'] = $next->toArray();
                if (is_numeric($phs['next']['url']) && $phs['next']['url'] > 0) {
                    $phs['next']['url'] = '[[~' . $phs['next']['url'] . ']]';
                }
                $phs['next']['view_url'] = $this->modx->makeUrl($singleImageResource, '', array(
                    $this->singleImageParam => $next->get('id'),
                ), $this->getProperty('scheme'));
                $phs['next']['crops'] = $next->getCropsAsArray();
            }

            $last = $image->getLast($this->getProperty('sortBy'), $this->getProperty('activeOnly'));
            if ($last instanceof mgImage) {
                $phs['last'] = $last->toArray();
                if (is_numeric($phs['last']['url']) && $phs['last']['url'] > 0) {
                    $phs['last']['url'] = '[[~' . $phs['last']['url'] . ']]';
                }
                $phs['last']['view_url'] = $this->modx->makeUrl($singleImageResource, '', array(
                    $this->singleImageParam => $last->get('id'),
                ), $this->getProperty('scheme'));
                $phs['last']['crops'] = $last->getCropsAsArray();
            }

            // If the sortorder is descending, we need to swap out prev and next placeholders to keep things in order
            if ($this->getProperty('sortDir') === 'DESC') {
                $prev = isset($phs['prev']) ? $phs['prev'] : null;
                $phs['prev'] = isset($phs['next']) ? $phs['next'] : null;
                $phs['next'] = $prev;
            }
            //ppb-todo do this for first an last
        }

        if ($this->getProperty('getTags')) {
            $phs['tags_raw'] = $image->getTags();
            $phs['tags'] = array();
            foreach ($phs['tags_raw'] as $tag) {
                $phs['tags'][] = $this->service->getChunk($this->getProperty('tagTpl'), $tag);
            }
            $phs['tags'] = implode($this->getProperty('tagSeparator'), $phs['tags']);
        }

        // Return it
        return $phs;
    }

    /**
     * Returns an array of resource fields (if the resource can be loaded, otherwise an empty array)
     *
     * @param $resourceId
     * @return array
     */
    public function getResourceFields($resourceId)
    {
        // See if we already have the resource data available as an array, if so return it.
        if (array_key_exists($resourceId, $this->_resourceData)) {
            return $this->_resourceData[$resourceId];
        }

        // See if we already have the resource, if not load it.
        if (!array_key_exists($resourceId, $this->_resources)) {
            $this->_resources[$resourceId] = $this->modx->getObject('modResource', (int)$resourceId);
        }

        // Local alias just to make it easier
        $resource = $this->_resources[$resourceId];

        // Make sure it's a modResource and then grab the data
        if ($resource instanceof modResource) {
            $data = $resource->get($this->resourceFields);

            // Only get the content if specifically requested
            if ($this->getProperty('getResourceContent')) {
                $data['content'] = $resource->get('content');
            }

            // Only get the properties field if specifically requested
            if ($this->getProperty('getResourceProperties')) {
                $data['properties'] = $resource->get('properties');
            }

            // Load the TVs that the user requested
            $tvs = $this->getProperty('getResourceTVs');
            if (!empty($tvs)) {
                $tvs = explode(',', $tvs);

                foreach ($tvs as $tv) {
                    $data[$tv] = $resource->getTVValue($tv);
                }
            }

            // Prefix 'resource.' to the values so we don't have to conflict with mgImage.resource
            $data = $this->prefix($data, 'resource.');

            // Store a local copy of it so we don't have to do this over and over
            $this->_resourceData[$resourceId] = $data;

            // Return it
            return $data;
        }
        return array();
    }

    /**
     * Prefixes the data with the specified prefix.
     *
     * @param $data
     * @return array
     */
    public function prefix($data, $prefix)
    {
        $prefixed = array();
        foreach ($data as $key => $value) {
            $prefixed[$prefix . $key] = $value;
        }
        return $prefixed;
    }

    /**
     * Finishes the snippet by either returning the snippet output or by setting a requested placeholder.
     *
     * @param $formatted
     * @return string
     */
    public function finish($formatted)
    {
        $this->debug('Finished processing, outputting results.');

        if ($this->getProperty('debug')) {
            $formatted .= '<h3>Debug Information</h3><pre>' . print_r($this->_debug, true) . '</pre>';
        }

        if ($this->getProperty('timing')) {
            $time = microtime(true);
            $spent = ($time - $this->_startTime) * 1000;
            $formatted .= '<p>Finished in ' . number_format($spent, 2) . 'ms</p>';
        }

        if ($placeholder = $this->getProperty('toPlaceholder')) {
            $this->modx->setPlaceholder($placeholder, $formatted);
            return '';
        }
        return $formatted;
    }

    /**
     * Gets a comma separated list of tags. This is either the defined tags in the snippet call,
     * or provided by a $_REQUEST parameter if &tagsFromUrl is specified and filled.
     *
     * @return string
     */
    public function getTags()
    {
        $rawTags = $this->getProperty('tags');
        $param = $this->getProperty('tagsFromUrl');
        if (isset($_REQUEST[$param])) {
            $rawTags = $_REQUEST[$param];
        }
        $rawTags = explode(',', $rawTags);

        // Sanitise tags
        $tags = array();
        foreach ($rawTags as $tag) {
            $tags[] = $this->modx->sanitizeString($tag);
        }

        return implode(',', $tags);
    }

    /**
     * This method adds the conditionals for tags to the collection query.
     *
     * @param xPDOQuery $c
     */
    public function addTagsCondition(xPDOQuery $c)
    {
        $allTagIds = $this->service->getTagIds();
        $tagIds = array();
        $excludedTagIds = array();

        // Loop over the requested tags to find their respective IDs
        $tags = $this->getProperty('tags');
        $tags = explode(',', $tags);
        if (count($tags) > 0) {
            $this->debug('Adding conditions for tags: ' . implode(', ', $tags));
            foreach ($tags as $tag) {
                $exclude = strpos($tag, '-') === 0;
                $tag = ($exclude) ? substr($tag, 1) : $tag;
                if (is_numeric($tag)) {
                    if (!$exclude) {
                        $tagIds[] = (int)$tag;
                    } else {
                        $excludedTagIds[] = (int)$tag;
                    }
                } else {
                    $search = array_search($tag, $allTagIds, true);
                    if (is_numeric($search) && $search > 0) {
                        if (!$exclude) {
                            $tagIds[] = $search;
                        } else {
                            $excludedTagIds[] = $search;
                        }
                    }
                }
            }


            // If we have tags to include, add 'm to the query
            if (!empty($tagIds)) {
                $this->debug('Including tag IDs: ' . implode(', ', $tagIds));
                $c->leftJoin('mgImageTag', 'Tags');
                $c->where(array(
                    'Tags.tag:IN' => $tagIds,
                ));
            }

            // If we have tags to exclude, get rid of 'm
            if (!empty($excludedTagIds)) {
                $this->debug('Excluding tag IDs: ' . implode(', ', $excludedTagIds));
                $excludedTagIds = implode(',', $excludedTagIds);
                $c->where(array(
                    "NOT EXISTS (SELECT 1 FROM {$this->modx->getTableName('mgImageTag')} Tags WHERE `mgImage`.`id` = `Tags`.`image` AND `Tags`.`tag` IN ({$excludedTagIds}))"
                ));
            }
        }
    }

    /**
     * Parses collection data into markup.
     *
     * @param $data
     * @param $total
     * @return string
     */
    public function parseCollection($data, $total)
    {
        $formattedArr = array();
        foreach ($data as $phs) {
            $tpl = $this->determineTpl($phs);
            $formattedArr[] = $this->service->getChunk($tpl, $phs);
        }
        $formatted = implode($this->getProperty('imageSeparator'), $formattedArr);

        // Apply the wrapper template
        $wrapperTpl = $this->getProperty('wrapperTpl');
        if (!empty($wrapperTpl)) {
            $wrapperIfEmpty = $this->getProperty('wrapperIfEmpty');
            if (!empty($formatted) || $wrapperIfEmpty) {
                $phs = array_merge(array(
                    'output' => $formatted,
                    'image_count' => $total,
                ), $this->getResourceFields($this->getProperty('resource')));

                $formatted = $this->service->getChunk($wrapperTpl, $phs);
            }
        }

        return $formatted;
    }

    /**
     * Returns the template (chunk name) to use for this item.
     *
     * @param $data
     * @return string
     */
    public function determineTpl($data)
    {
        switch ($data['class_key']) {
            case 'mgYouTubeVideo':
            case 'mgVideo':
                return $this->getProperty('youtubeTpl');

            case 'mgVimeoVideo':
                return $this->getProperty('vimeoTpl');

            case 'mgImage':
            default:
                return $this->getProperty('imageTpl');
        }
    }

    /**
     * Returns the template (chunk name) to use for a single item being displayed.
     *
     * @param $data
     * @return string
     */
    public function determineSingleTpl($data)
    {
        switch ($data['class_key']) {
            case 'mgYouTubeVideo':
            case 'mgVideo':
                return $this->getProperty('singleYoutubeTpl');

            case 'mgVimeoVideo':
                return $this->getProperty('singleVimeoTpl');

            case 'mgImage':
            default:
                return $this->getProperty('singleImageTpl');
        }
    }

    /**
     * Generates a sha1 hash for the (unparsed) content of all chunks used by this snippet call. This is used for
     * automatically busting the relevant caches when a chunk is changed.
     *
     * @return string
     */
    public function getChunkHash()
    {
        if (empty($this->chunkHash)) {
            $props = array(
                'tagTpl',
                'imageTpl',
                'youtubeTpl',
                'vimeoTpl',
                'singleImageTpl',
                'singleYoutubeTpl',
                'singleVimeoTpl'
            );
            $chunks = array();
            foreach ($props as $tplProp) {
                $chunks[] = $this->getProperty($tplProp);
            }

            $chunks = array_unique($chunks);

            $this->debug('Calculating chunkHash (for automatic cache-busting) for chunks: ' . implode(',', $chunks));

            $c = $this->modx->newQuery('modChunk');
            $c->where(array('name:IN' => $chunks));
            $c->select($this->modx->getSelectColumns('modChunk', 'modChunk', '', array('id', 'name', 'snippet')));

            $c->prepare();
            $sql = $c->toSQL();

            $chunkContents = array();
            $result = $this->modx->query($sql);
            while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
                $chunkContents[] = $row['snippet'];
            }

            $chunkContents = implode(',', $chunkContents);
            $this->chunkHash = sha1(sha1($chunkContents));

            $chunkContents = str_replace(array('[', ']'), array('&#91;', '&#93;'), htmlentities($chunkContents, ENT_QUOTES, 'utf-8'));
            $this->debug('chunkHash calculated as ' . $this->chunkHash . ' based on raw content ' . $chunkContents);
        }

        return $this->chunkHash;
    }

}