Source: interaction/LayersFilters.js

import Interaction from 'ol/interaction/Interaction'
import Collection from 'ol/Collection'
import { unByKey } from 'ol/Observable'
import compact from 'lodash/compact'

/**
 * Evalue une condition (prédicat) pour une valeur donnée
 * Renvoie true ou false
 * @param predicateType <String> Type de prédicat (opérateur)
 * @param predicateValues Valeurs possibles pour le type de prédicat (simple valeur ou tableau de valeurs)
 * @param value Valeur à tester
 */
const matchPredicate = function (predicateType, predicateValues, value) {
  switch (predicateType.toLowerCase()) {
    case 'invalues': // vrai si la valeur de la propriété est présente dans le tableau
      return predicateValues.some((val) => val === value)
    case 'notinvalues': // vrai si la valeur de la propriété est absente du tableau
      return !(predicateValues.some((val) => val === value))
    case 'equal': // vrai si la valeur de la propriété est égale à une valeur donnée
      return (predicateValues === value)
    case 'notequal': // vrai si la valeur de la propriété est différente d'une valeur donnée
      return (predicateValues !== value)
    default:
      console.warn(`type non identifié ${predicateType.toLowerCase()}`)
      return false
  }
}

function isFeatureVisible (feature, rules) {
  const properties = feature.getProperties()

  const isVisible = rules.every(({ type, values, field }) => {
    return matchPredicate(type, values, properties[field])
  })
  return isVisible
}

class LayersFilters extends Interaction {
  /**
   *  @type {ol.Collection}
   * */
  filtersOrGroups = new Collection()

  /**
   * référence des features ayant une filtre appliqué
   */
  hiddenFeatures = []

  constructor (options) {
    options = options || {}

    // on utilise self car on ne peut pas utiliser this avant la méthode super()
    // const self = null

    super(options = options || {})

    this.viewer = options.viewer
    this.filtersOrGroups.on('add', this.handleFilterChanged)
    this.filtersOrGroups.on('remove', this.handleFilterChanged)
  }

  handleFilterChanged () {
    this.dispatchEvent({ type: 'filters:change' })
    this.changed()
  }

  /**
   * Ajoute un groupe
   * @param {string} idGroup
   * @param {Object} param1
   */
  addGroup (idGroup, { label, sublabel }) {
    const group = {
      type: 'group',
      id: idGroup,
      label,
      sublabel,
      filters: new Collection(),
    }
    this.filtersOrGroups.push(group)
    group.filters.on('add', this.handleFilterChanged)
    group.filters.on('remove', this.handleFilterChanged)
    return group
  }

  /**
   * ajoute un filtre
   * @param {string} idFilter
   * @param {Object} param1
   */
  addFilter (idFilter, { idGroup, label, sublabel }) {
    if (this.filterExists(idFilter)) {
      throw new Error(`[LayersFilters:addFilter] Le filtre ${idFilter} existe déjà`)
    }
    // attention a ne jamais remplacer les instances de filters par une copie, on se sert d'une égalité de réference dans ControlLayerFilter
    const filter = {
      type: 'filter',
      id: idFilter,
      label,
      sublabel,
      applied: false,
      layerFilters: {},
      listeners: [], // écouteurs sur les sources de calques pour vérifier si on a des ajout/suppression sur le calque
    }

    if (idGroup) {
      const group = this.filtersOrGroups.getArray().find(({ type, id }) => type === 'group' && id === idGroup)
      if (!group) {
        throw new Error(`[LayersFilters:addFilter] Le groupe ${idGroup} n'existe pas`)
      }
      group.filters.push(filter)
      return filter
    }
    this.filtersOrGroups.push(filter)
    return filter
  }

  removeAllGroupsAndFilters () {
    this.clear()
    this.filtersOrGroups.clear()
    this.handleFilterChanged()
  }

  /**
   *
   * @param {string | object} filterOrIdFilter
   * @param {object} layerFilters @example
    {
        "cans": [
            {
                "type": "inValues", // equal | notequal | invalues | notinvalues
                "field": "TYPRES", // nom du champ de la feature openlayers
                "values": [ // valeur ou liste de valeurs
                    "EU"
                ]
            }
        ],
        "ouvs": [
            {
                "type": "inValues",
                "field": "TYPRES",
                "values": [
                    "EU"
                ]
            }
        ]
    }
   */
  addLayers (filterOrIdFilter, layerFilters) {
    const { idFilter, ...options } = filterOrIdFilter

    const filter = typeof filterOrIdFilter === 'object' ? this.addFilter(idFilter, options) : this.getFilter(filterOrIdFilter)

    Object.keys(layerFilters).forEach(layerName => {
      filter.layerFilters[layerName] = compact([].concat(filter.layerFilters[layerName], layerFilters[layerName]))
    })
    this.handleFilterChanged()
  }

  /**
   * Est-ce que cette sublabel de filtre existe déjà ?
   * @param {string} idFilter
   * @returns {boolean}
   */
  filterExists (idFilter) {
    return this.filtersOrGroups.getArray().some(({ type, id, filters }) => {
      if (type === 'filter' && id === idFilter) {
        return true
      }
      if (type === 'group') {
        return filters.getArray().some(({ id }) => id === idFilter)
      }
      return false
    })
  }

  /**
   *
   * @param {string} idFilter
   * @returns {object}
   */
  getFilter (idFilter) {
    return this.filtersOrGroups.getArray().reduce((acc, filterOrGroup) => {
      if (acc) {
        return acc // déjà trouvé
      }
      const { type, id, filters } = filterOrGroup
      if (type === 'filter' && id === idFilter) {
        return filterOrGroup
      }
      if (type === 'group') {
        return filters.getArray().find(({ id }) => id === idFilter)
      }
      return undefined
    }, null)
  }

  getAllFilters () {
    return this.filtersOrGroups.getArray().reduce((acc, filterOrGroup) => {
      const { type, filters } = filterOrGroup
      if (type === 'filter') {
        acc.push(filterOrGroup)
      }
      if (type === 'group') {
        acc = acc.concat(filters.getArray())
      }
      return acc
    }, [])
  }

  /**
   *
   * @returns filtre appliqué
   */
  getCurrentFilter () {
    return this.getAllFilters().find(({ applied }) => applied)
  }

  clear () {
    this.removeCurrentFilter()
    this.viewer.refreshMap()
  }

  removeCurrentFilter () {
    this.hiddenFeatures.forEach(feature => this.viewer.dataLayer.showFeature(feature, null, true))
    this.hiddenFeatures = []
    const filter = this.getCurrentFilter()
    if (!filter) {
      return
    }
    filter.listeners.forEach(unByKey)
    filter.applied = false
    this.dispatchEvent({ type: 'change:visible', filter, visible: false })
  }

  applyFilter (idFilter) {
    this.removeCurrentFilter()
    const filter = this.getFilter(idFilter)
    if (!filter) {
      console.warn(`[LayersFilters:addFilter] Filtre ${idFilter} non trouvé`)
      return
    }
    // on va chercher les features a masquer grâce aux rules sur le layer
    const newHiddenFeatures = []
    Object.keys(filter.layerFilters).forEach(layerName => {
      const rules = filter.layerFilters[layerName]
      this.viewer.dataLayer.getFeatures(layerName).forEach(feature => {
        const isVisible = isFeatureVisible(feature, rules)

        if (!isVisible) {
          this.viewer.dataLayer.hideFeature(feature, null, true)
          newHiddenFeatures.push(feature)
        }
      })
      // on vérifi si il y a des nouvelles features sur le calque pour appliquer le filtre
      const source = this.viewer.dataLayer.getSourceLayer(layerName)
      filter.listeners.push(source.on('addfeature', ({ feature }) => {
        const isVisible = isFeatureVisible(feature, rules)

        if (!isVisible) {
          this.viewer.dataLayer.hideFeature(feature, null, false)
          this.hiddenFeatures.push(feature)
        }
      }))
      filter.listeners.push(source.on('changefeature', ({ feature }) => {
        const isVisible = isFeatureVisible(feature, rules)

        const hiddenFeature = this.hiddenFeatures.find(hiddenFeature => hiddenFeature === feature)
        if (!isVisible && !hiddenFeature) {
          // vérifi si on ne l'a pas déjà masquée avant son changement de valeur
          this.viewer.dataLayer.hideFeature(feature, null, false)
          this.hiddenFeatures.push(feature)
        } else if (isVisible && hiddenFeature) {
          this.viewer.dataLayer.showFeature(feature, null, false)
          this.hiddenFeatures = this.hiddenFeatures.filter(hiddenFeature => hiddenFeature !== feature)
        }
      }))
      filter.listeners.push(source.on('removefeature', ({ feature }) => {
        this.hiddenFeatures = this.hiddenFeatures.filter(hiddenFeature => hiddenFeature !== feature)
      }))
    })
    this.hiddenFeatures = newHiddenFeatures
    this.viewer.refreshMap()
    filter.applied = true
    this.dispatchEvent({ type: 'change:visible', filter, visible: true })
  }

  getGroupAndFilters () {
    return this.filtersOrGroups
  }
}

export default LayersFilters