app/resto/core/addons/Tag.php

Summary

Maintainability
D
1 day
Test Coverage
<?php
/*
 * Copyright 2018 Jérôme Gasperi
 *
 * Licensed under the Apache License, version 2.0 (the "License");
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

/**
 * Tag add-on
 *
 * This add-on compute tags from feature.
 *
 * It requires the iTag library (https://github.com/jjrom/itag)
 *
 */
class Tag extends RestoAddOn
{
    /**
     * Constructor
     *
     * @param RestoContext $context
     * @param RestoContext $user
     */
    public function __construct($context, $user)
    {
        parent::__construct($context, $user);
    }

    /**
     * Compute keywords from properties array
     *
     * @param array $properties
     * @param array $geometry (GeoJSON geometry)
     * @param RestoModel $model
     * @param array $iTagParams
     */
    public function getKeywords($properties, $geometry, $model, $iTagParams)
    {
        return $iTagParams && $iTagParams['taggers'] ? array_merge($this->keywordsFromITag($properties, $geometry, $iTagParams), $this->keywordsFromProperties($properties, $model)) : $this->keywordsFromProperties($properties, $model);
    }

    /**
     * Return a RESTo keywords array from an iTag Hierarchical feature
     *
     * @param array $properties
     * @param array $geometry (GeoJSON)
     * @param array $iTagParams
     */
    private function keywordsFromITag($properties, $geometry, $iTagParams)
    {
        /*
         * No geometry = no iTag
         *
         * [TODO] Add support to null geometry in iTag instead
         */
        if (! isset($geometry)) {
            return array();
        }

        $taggerKeys = array_keys($iTagParams['taggers']);
        $queryParams = array(
            'geometry' => RestoGeometryUtil::geoJSONGeometryToWKT($geometry),
            'taggers' => join(',', $taggerKeys),
            'planet' => $iTagParams['planet'] ?? $this->context->core['planet']
        );

        if (isset($properties['startDate'])) {
            $queryParams['timestamp'] = $properties['startDate'];
        }

        /*
         * Convert taggers options to query params
         */
        for ($i = count($taggerKeys); $i--;) {
            foreach ($iTagParams['taggers'][$taggerKeys[$i]] as $optionName => $optionValue) {
                $queryParams[strtolower($taggerKeys[$i]) . '_' . $optionName] = $optionValue;
            }
        }
        
        try {
            $curl = new Curly();
            $iTagFeature = json_decode($curl->get($this->options['iTag']['endpoint'] . '?' . http_build_query($queryParams)), true);
            $curl->close();
        } catch (Exception $e) {
            $curl->close();
            RestoLogUtil::httpError($e->getCode(), $e->getMessage());
        }
        
        /*
         * Return empty result
         */
        if (!isset($iTagFeature) || !isset($iTagFeature['content'])) {
            return array();
        }

        return $this->processITagKeywords($iTagFeature);
    }

    /**
     * Return all keywords from iTag process
     *
     * @param array $iTagFeature
     */
    private function processITagKeywords($iTagFeature)
    {
        /*
         * Initialize keywords array from faceted properties
         */
        $keywords = array();

        /*
         * Process keywords
         */
        foreach ($iTagFeature['content'] as $key => $value) {
            switch ($key) {
                case 'political':
                    if (isset($value['continents'])) {
                        $keywords = array_merge($keywords, $this->getGenericKeywords($value['continents'], array(
                            'defaultName' => null,
                            'parentId' => null
                        )));
                    }
                    break;

                case 'landcover':
                    $keywords = array_merge($keywords, $this->getLandCoverKeywords($value));
                    break;

                case 'population':
                    $keywords = array_merge($keywords, $this->getPopulationKeywords($value));
                    break;
                
                case 'keywords':
                    $keywords = array_merge($keywords, $this->getAlwaysKeywords($value));
                    break;

                default:
                    if (is_array($value)) {
                        $keywords = array_merge($keywords, $this->getGenericKeywords($value, array(
                            'defaultName' => null,
                            'parentId' => null
                        )));
                    }
            }
        }

        return $keywords;
    }

    /**
     * Get keywords from iTag 'keywords' property
     *
     * @param array $properties
     */
    private function getAlwaysKeywords($properties)
    {
        $keywords = array();
        foreach (array_values($properties) as $id) {
            list($type, $normalized) = explode(RestoConstants::TAG_SEPARATOR, $id, 2);
            if (!$this->alreadyExists($keywords, $id)) {
                $keywords[] = array(
                    'id' => $id,
                    'name' => ucfirst($normalized),
                    'type' => $type
                );
            }
        }
        return $keywords;
    }

    /**
     * Get Landcover keywords
     *
     * @param array $properties
     */
    private function getLandCoverKeywords($properties)
    {
        $keywords = array();

        /*
         * Main landcover
         */
        if (isset($properties['main'])) {
            foreach (array_values($properties['main']) as $landcover) {
                $id = $landcover['id'];
                list($type) = explode(RestoConstants::TAG_SEPARATOR, $landcover['id'], 1);
                if (!$this->alreadyExists($keywords, $id)) {
                    $keywords[] = array(
                        'id' => $id,
                        'name' => $landcover['name'],
                        'type' => $type,
                        'area' => $landcover['area'],
                        'value' => $landcover['pcover']
                    );
                }
            }
        }

        return $keywords;
    }

    /**
     * Get generic keywords
     *
     * @param array $properties
     * @param array $options
     * @return array
     */
    private function getGenericKeywords($properties, $options)
    {
        $keywords = array();
        for ($i = 0, $ii = count($properties); $i < $ii; $i++) {
            $keyword = $this->getGenericKeyword($properties[$i], $options['defaultName'], $options['parentId']);
            if (! $this->alreadyExists($keywords, $keyword['id'])) {
                $keywords[] = $keyword;
                switch ($keyword['type']) {
                    case 'continent':
                        $keywords = array_merge($keywords, $this->getGenericKeywords($properties[$i]['countries'], array(
                            'parentId' => $keyword['id'],
                            'defaultName' => null
                        )));
                        break;
                    case 'country':
                        if (isset($properties[$i]['regions'])) {
                            $keywords = array_merge($keywords, $this->getGenericKeywords($properties[$i]['regions'], array(
                                'parentId' => $keyword['id'],
                                'defaultName' => '_all'
                            )));
                        }
                        break;
                    case 'region':
                        $keywords = array_merge($keywords, $this->getGenericKeywords($properties[$i]['states'], array(
                            'parentId' => $keyword['id'],
                            'defaultName' => '_unknown'
                        )));
                        break;
                    default:
                        break;
                }
            }
        }

        return $keywords;
    }

    /**
     * Get generic keyword
     *
     * @param array $property
     * @param string $defaultName
     * @param string $parentId
     *
     */
    private function getGenericKeyword($property, $defaultName, $parentId)
    {
        $exploded = explode(RestoConstants::TAG_SEPARATOR, $property['id']);

        $keyword = array(
            'id' => $property['id'],
            'name' => $property['name'] ?? $defaultName,
            'type' => $exploded[0]
        );
        if (isset($parentId)) {
            $keyword['parentId'] = $parentId;
        }
        if (isset($property['area'])) {
            $keyword['value'] = $property['area'];
        }
        if (isset($property['pcover'])) {
            $keyword['value'] = $property['pcover'];
        }
        /*
         * Absolute coverage of geographical entity
         */
        if (isset($property['gcover'])) {
            $keyword['gcover'] = $property['gcover'];
        }

        return $keyword;
    }

    /**
     * Return a RESTo keywords array from feature properties
     *
     * @param array $properties
     * @param RestoModel $model
     */
    private function keywordsFromProperties($properties, $model)
    {
        /*
         * [IMPORTANT] If input properties contains a _keywords property then use it
         */
        $keywords = $properties['_keywords'] ?? array();

        /*
         * Roll over facet categories
         */
        foreach (array_values($model->facetCategories) as $facetCategory) {
            $keywords = array_merge($keywords, $this->keywordsFromFacets($properties, $facetCategory, $model));
        }

        /*
         * Get date keywords
         */
        return array_merge($keywords, $this->getDateKeywords($properties, $model));
    }

    /**
     * Process keywords for facets
     *
     * @param array $properties
     * @param array $facetCategory
     * @param RestoModel $model
     * @return array
     */
    private function keywordsFromFacets($properties, $facetCategory, $model)
    {
        $parentId = null;
        $keywords = array();
        for ($i = 0, $ii = count($facetCategory); $i < $ii; $i++) {
            // Get value in properties for input facetCategory
            $value = $properties[$facetCategory[$i]] ?? null;
            
            // If the facetCategory is not found, try the STAC alias
            if (!isset($value) && isset($model->stacMapping[$facetCategory[$i]])) {
                $value = $properties[$model->stacMapping[$facetCategory[$i]]['key']] ?? null;
            }

            if (isset($value)) {
                // If input property is an array then split into individuals values
                // This is to support "instruments" for instance
                // [IMPORTANT] Only a leaf (i.e. the last child) can be an array with more than 1 element
                // otherwise parentId computation can be erroneous
                if (! is_array($value)) {
                    $value = array($value);
                }
                
                $newParentId = null;
                for ($j = 0, $jj = count($value); $j < $jj; $j++) {
                    $keyword = array();
                    // [IMPORTANT] Discard all spaces from value
                    $id = $facetCategory[$i] . RestoConstants::TAG_SEPARATOR . str_replace(' ', '', $value[$j]);
                    if (! $this->alreadyExists($keywords, $id)) {
                        $keyword = array(
                            'id' => $id,
                            'name' => $value[$j],
                            'type' => $facetCategory[$i],
                        );
                        if (isset($parentId)) {
                            $keyword['parentId'] = $parentId;
                        }
                        $keywords[] = $keyword;
                        $newParentId = $id;
                    }
                }
                if ($newParentId) {
                    $parentId = $id;
                }
            } else {
                $parentId = null;
            }
        }
        return $keywords;
    }

    /**
     * Process date keywords
     *
     * @param array $properties
     * @param RestoModel $model
     * @return array
     */
    private function getDateKeywords($properties, $model)
    {

        $startDate = $properties[$model->searchFilters['time:start']['key']];

        /*
         * No startDate property or both startDate and completionDate are presents => range is not supported
         */
        if (! isset($startDate) || (isset($startDate) && isset($properties['completionDate']))) {
            return array();
        }

        /*
         * Year
         */
        $year = substr($startDate, 0, 4);

        /*
         * Month
         */
        $month = substr($startDate, 5, 2);

        /*
         * Day
         */
        $day = substr($startDate, 8, 2);

        return array(
            array(
                'id' => 'year' . RestoConstants::TAG_SEPARATOR . $year,
                'name' => $year,
                'type' => 'year'
            ),
            array(
                'id' =>  'month' . RestoConstants::TAG_SEPARATOR . $month,
                'name' => $month,
                'type' => 'month'
            ),
            array(
                'id' => 'day' . RestoConstants::TAG_SEPARATOR . $day,
                'name' => $day,
                'type' => 'day'
            )
        );
    }

    /**
     * Get keywords from iTag 'population' property
     *
     * @param array $populationProperty
     */
    private function getPopulationKeywords($populationProperty)
    {
        return array(
            array(
                'id' => 'other:population',
                'name' => 'Population',
                'type' => 'other',
                'count' => $populationProperty['count'],
                'densityPerSquareKm' => $populationProperty['densityPerSquareKm']
            )
        );
    }

    /**
     * Return true if id exists in keywords array
     *
     * @param array $keywords
     * @param string $identifier
     */
    private function alreadyExists($keywords, $identifier)
    {
        for ($i = count($keywords); $i--;) {
            if ($identifier === $keywords[$i]['id']) {
                return true;
            }
        }
        return false;
    }
}