Smile-SA/elasticsuite

View on GitHub
src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php
/**
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer
 * versions in the future.
 *
 * @category  Smile
 * @package   Smile\ElasticsuiteCatalog
 * @author    Aurelien FOUCRET <aurelien.foucret@smile.fr>
 * @copyright 2020 Smile
 * @license   Open Software License ("OSL") v. 3.0
 */
namespace Smile\ElasticsuiteCatalog\Block\Navigation\Renderer;

use Magento\Catalog\Helper\Data as CatalogHelper;
use Magento\Framework\Json\EncoderInterface;
use Magento\Framework\Locale\FormatInterface;
use Magento\Framework\View\Element\Template\Context;
use Smile\ElasticsuiteCatalog\Helper\Slider as CatalogSliderHelper;
use Smile\ElasticsuiteCatalog\Model\Layer\Filter\Decimal;

/**
 * This block handle standard decimal slider rendering.
 *
 * @category Smile
 * @package  Smile\ElasticsuiteCatalog
 * @author   Aurelien FOUCRET <aurelien.foucret@smile.fr>
 * @author   Romain Ruaud <romain.ruaud@smile.fr>
 */
class Slider extends AbstractRenderer
{
    /**
     * The Data role, used for Javascript mapping of slider Widget
     *
     * @var string
     */
    protected $dataRole = "range-slider";

    /**
     * @var EncoderInterface
     */
    private $jsonEncoder;

    /**
     * @var FormatInterface
     */
    protected $localeFormat;

    /**
     * @var CatalogSliderHelper
     */
    protected $catalogSliderHelper;

    /**
     *
     * @param Context             $context             Template context.
     * @param CatalogHelper       $catalogHelper       Catalog helper.
     * @param EncoderInterface    $jsonEncoder         JSON Encoder.
     * @param FormatInterface     $localeFormat        Price format config.
     * @param CatalogSliderHelper $catalogSliderHelper Catalog slider helper.
     * @param array               $data                Custom data.
     */
    public function __construct(
        Context $context,
        CatalogHelper $catalogHelper,
        EncoderInterface $jsonEncoder,
        FormatInterface $localeFormat,
        CatalogSliderHelper $catalogSliderHelper,
        array $data = []
    ) {
        parent::__construct($context, $catalogHelper, $data);

        $this->jsonEncoder         = $jsonEncoder;
        $this->localeFormat        = $localeFormat;
        $this->catalogSliderHelper = $catalogSliderHelper;
    }

    /**
     * Return the config of the price slider JS widget.
     *
     * @return string
     */
    public function getJsonConfig()
    {
        $config = $this->getConfig();

        return $this->jsonEncoder->encode($config);
    }

    /**
     * Retrieve the data role
     *
     * @return string
     */
    public function getDataRole()
    {
        $filter = $this->getFilter();

        return $this->dataRole . "-" . $filter->getRequestVar();
    }

    /**
     * Show adaptive slider ?
     *
     * @return bool
     * @SuppressWarnings(PHPMD.ShortVariable)
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    public function showAdaptiveSlider(): bool
    {
        $showAdaptiveSlider = false;
        if ($this->catalogSliderHelper->isAdaptiveSliderEnabled()
            && ($this->getFilter()->getItemsCount() >= CatalogSliderHelper::ADAPTIVE_MINIMUM_ITEMS)
        ) {
            $hasDispersedData = false;
            try {
                $layer = $this->getFilter()->getLayer();
                $attributeModel = $this->getFilter()->getAttributeModel();
                if ($layer && $attributeModel) {
                    $facetName = $this->catalogSliderHelper->getStatsAggregation($attributeModel->getAttributeCode());
                    $stats = $layer->getProductCollection()->getFacetedData($facetName);
                    $stats = current($stats);
                    /* Coefficient of Variation */
                    $cv = ($stats['std_deviation'] ?? 0) / ($stats['avg'] ?? 1);
                    $hasDispersedData = ($cv > 1.0);
                    $lowerStdDevBound = $stats['std_deviation_bounds']['lower'] ?? 0;
                    $upperStdDevBound = $stats['std_deviation_bounds']['upper'] ?? 0;
                    if ($lowerStdDevBound && $upperStdDevBound) {
                        $hasDispersedData = (
                            $hasDispersedData || (
                                ($this->getMinValue() < $lowerStdDevBound)
                                || ($this->getMaxValue() > $upperStdDevBound)
                            )
                        );
                    }
                }
            } catch (\Magento\Framework\Exception\LocalizedException $e) {
                ;
            }
            $showAdaptiveSlider = $hasDispersedData;
        }

        return $showAdaptiveSlider;
    }

    /**
     * {@inheritDoc}
     */
    protected function canRenderFilter()
    {
        return $this->getFilter() instanceof Decimal;
    }

    /**
     * Retrieve Field Format for slider display
     *
     * @return array
     */
    protected function getFieldFormat()
    {
        $format = $this->localeFormat->getPriceFormat();

        $attribute = $this->getFilter()->getAttributeModel();

        $format['pattern']           = (string) $attribute->getDisplayPattern();
        $format['precision']         = (int) $attribute->getDisplayPrecision();
        $format['requiredPrecision'] = (int) $attribute->getDisplayPrecision();
        $format['integerRequired']   = (int) $attribute->getDisplayPrecision() > 0;

        return $format;
    }

    /**
     * Retrieve configuration
     *
     * @return array
     */
    protected function getConfig()
    {
        $config = [
            'minValue'           => $this->getMinValue(),
            'maxValue'           => $this->getMaxValue(),
            'currentValue'       => $this->getCurrentValue(),
            'fieldFormat'        => $this->getFieldFormat(),
            'intervals'          => $this->getIntervals(),
            'adaptiveIntervals'  => $this->getAdaptiveIntervals(),
            'showAdaptiveSlider' => $this->showAdaptiveSlider(),
            'urlTemplate'        => $this->getUrlTemplate(),
            'messageTemplates'   => [
                'displayOne'   => __('1 product'),
                'displayCount' => __('<%- count %> products'),
                'displayEmpty' => __('No products in the selected range.'),
            ],
        ];

        return $config;
    }

    /**
     * Returns min value of the slider.
     *
     * @return int
     */
    protected function getMinValue()
    {
        return $this->getFilter()->getMinValue();
    }

    /**
     * Returns max value of the slider.
     *
     * @return int
     */
    protected function getMaxValue()
    {
        return $this->getFilter()->getMaxValue() + 1;
    }

    /**
     * Returns values currently selected by the user.
     *
     * @return array
     */
    private function getCurrentValue()
    {
        $currentValue = $this->getFilter()->getCurrentValue();

        if (!is_array($currentValue)) {
            $currentValue = [];
        }

        if (!isset($currentValue['from']) || $currentValue['from'] === '') {
            $currentValue['from'] = $this->getMinValue();
        }

        if (!isset($currentValue['to']) || $currentValue['to'] === '') {
            $currentValue['to'] = $this->getMaxValue();
        }

        return $currentValue;
    }

    /**
     * Return available intervals.
     *
     * @return array
     */
    private function getIntervals()
    {
        $intervals = [];
        foreach ($this->getFilter()->getItems() as $item) {
            $intervals[] = ['value' => $item->getValue(), 'count' => $item->getCount()];
        }

        return $intervals;
    }

    /**
     * Return available adaptive intervals.
     *
     * @return array
     */
    private function getAdaptiveIntervals(): array
    {
        $adaptiveIntervals = [];
        if ($this->showAdaptiveSlider()) {
            $adaptiveIntervals = $this->prepareAdaptiveIntervals();
        }

        return $adaptiveIntervals;
    }

    /**
     * Prepare adaptive intervals.
     *
     * @return array
     */
    private function prepareAdaptiveIntervals(): array
    {
        $adaptiveIntervals = [];
        $intervals = $this->getIntervals();

        $totalCount = array_sum(array_column($intervals, 'count'));
        // Cumulative Distribution Function Value.
        $cdfValue = 0;
        $keys = [];
        $keyValues = [];
        foreach ($intervals as $interval) {
            // We use cumulative distribution function to create the adaptive intervals.
            $value = ($interval['count'] / $totalCount) * 100;
            $cdfValue += $value;
            $key = (int) floor($cdfValue);
            $keys[$key] = $key;
            $keyValues[$key] = ['key' => $key, 'value' => $interval['value']];
            $adaptiveIntervals[(string) $cdfValue] = [
                'originalValue' => $interval['value'],
                'value'         => $cdfValue,
                'count'         => $interval['count'],
            ];
        }

        if (!empty($adaptiveIntervals)) {
            $keys = array_values($keys);
            $length = count($keyValues);
            $missingSlots = [];
            for ($i = 0; $i < ($length - 1); $i++) {
                $left  = $keys[$i];
                $right = $keys[$i + 1];
                $cdfRange   = $keyValues[$right]['key'] - $keyValues[$left]['key'];
                $priceRange = $keyValues[$right]['value'] - $keyValues[$left]['value'];
                $priceStep  = $priceRange / $cdfRange;
                for ($j = 1; $j < $cdfRange; $j++) {
                    $cdfKey = $keyValues[$left]['key'] + $j;
                    $price  = $keyValues[$left]['value'] + ($priceStep * $j);
                    $missingSlots[(string) $cdfKey] = [
                        'originalValue' => $price,
                        'value'         => $cdfKey,
                        'count'         => 0,
                    ];
                }
            }

            $maxIntervalKey = (string) $cdfValue;
            $adaptiveIntervals[(string) 101] = [
                'originalValue' => $adaptiveIntervals[$maxIntervalKey]['originalValue'] + 1,
                'value' => 101,
                'count' => 0,
            ];
            // Fill up the intermediate step.
            $adaptiveIntervals = $adaptiveIntervals + $missingSlots;
            ksort($adaptiveIntervals);

            $adaptiveIntervals = array_values($adaptiveIntervals);
        }

        return $adaptiveIntervals;
    }

    /**
     * Retrieve filter URL template with placeholders for range.
     *
     * @return string
     */
    private function getUrlTemplate()
    {
        $filter = $this->getFilter();
        $item   = current($this->getFilter()->getItems());

        $regexp      = "/({$filter->getRequestVar()})=(-?[0-9]+)/";
        $replacement = '${1}=<%- from %>-<%- to %>';

        return preg_replace($regexp, $replacement, $item->getUrl());
    }
}