Source: ol/source/FilterAndSort.js

/**
 * @module nyc/ol/source/FilterAndSort
 */

import $ from 'jquery'

import {get as olProjGetProjection} from 'ol/proj'
import OlGeomLineString from 'ol/geom/LineString'
import AutoLoad from 'nyc/ol/source/AutoLoad'

import {get as olProjGet} from 'ol/proj'

const FEET_PRE_METER = 3.28084

/**
 * @desc Class to filter and sort features
 * @public
 * @class
 * @extends Autoload
 */
class FilterAndSort extends AutoLoad {
  /**
   * @desc Create an instance of FilterAndSort
   * @public
   * @constructor
   * @param {olx.source.VectorOptions} options Constructor options
   * @param {boolean} [metric=false] Use metric units
   */
  constructor(options, metric) {
    super(options)
    /**
     * @private
     * @member {boolean}
     */
    this.metric = metric
    /**
     * @private
     * @member {Array<ol.Feature>}
     */
    this.allFeatures = options.features || []
    this.storeFeatures = this.storeFeatures.bind(this);
    this.on('change:autoload-complete', this.storeFeatures)
  }
  /**
   * @desc Filters the features of this source
   * @public
   * @method
   * @param {Array<Array<module:nyc/ol/source/FilterAndSort~FilterAndSort.Filter>>} filters Used to filter features by attributes
   * @return {Array<ol.Feature>} An array of features contained in this source that are the result of the intersection of the applied filters
   */
  filter(filters) {
    const filteredFeatures = this.allFeatures.filter(feature => {
      return filters.every(fltrs => {
        return fltrs.some(flt => {
          return $.inArray(feature.get(flt.property), flt.values) > -1
        })
      })
    })
    this.clear(true)
    this.addFeatures(filteredFeatures)
    return filteredFeatures
  }
  /**
   * @private
   * @method
   * @param {ol.Coordinate} coordinate The coordinate from which to measure distance to features
   * @param {Number=} count The number of features to return
   * @return {Array<ol.Feature>} The features, each decorated with a getDistance function that returns a {@link module:nyc/ol/source/FilterAndSort~FilterAndSort.Distance} object
   */
  doSort(coordinate, count) {
    const features = this.getFeatures()
    features.sort((f0, f1) => {
      const dist0 = this.distance(coordinate, f0.getGeometry())
      const dist1 = this.distance(coordinate, f1.getGeometry())
      if (count === undefined) {
        f0.set('__distance', dist0)
        f1.set('__distance', dist1)
        $.extend(f0, FilterAndSort.DistanceDecoration)
        $.extend(f1, FilterAndSort.DistanceDecoration)
      }
      if (dist0.distance < dist1.distance) {
        return -1
      } else if (dist0.distance > dist1.distance) {
        return 1
      }
      return 0
    })
    if (count > 0) {
      return features.slice(0, count)
    }
    return features
  }
  /**
   * @desc Sort features by distance from a coordinate
   * @public
   * @method
   * @param {ol.Coordinate} coordinate The coordinate from which to measure distance to features
   * @return {Array<ol.Feature>} The features, each decorated with a getDistance function that returns a {@link module:nyc/ol/source/FilterAndSort~FilterAndSort.Distance} object
   */
  sort(coordinate) {
    return this.doSort(coordinate)
  }
  /**
   * @desc Get nearest n features from a coordinate
   * @public
   * @method
   * @param {ol.Coordinate} coordinate The coordinate from which to measure distance to features
   * @param {Number=} count The number of features to return
   * @return {Array<ol.Feature>} The features
   */
  nearest(coordinate, count) {
    return this.doSort(coordinate, count)
  }
  /**
   * @private
   * @method
   * @param {ol.Coordinate} coordinate The OpenLayers coordinate object
   * @param {ol.geom.Geometry} geom The OpenLayers geometry object
   * @return {module:nyc/FilterAndSort~FilterAndSort.Distance} Distance object
   */
  distance(coordinate, geom) {
    const line = new OlGeomLineString([coordinate, geom.getClosestPoint(coordinate)])
    const projections = this.projections(this.getFormat())
    let units = projections[1].getUnits()
    if (projections[0] && projections[0].getUnits() !== 'degrees') {
      line.transform(projections[1], projections[0])
      units = projections[0].getUnits()
    }
    if (units !== 'ft' && !this.metric) {
      return {distance: line.getLength() * FEET_PRE_METER, units: 'ft'}
    }
    return {distance: line.getLength(), units: units}
  }
  /**
   * @private
   * @method
   * @param {ol.format.Feature} format OpenLayers format
   * @return {Array<ol.proj.Projection>} Array of projections
   */
  projections(format) {
    const parentFormat = format ? format.parentFormat : null
    let dataProj = 'EPSG:3857'
    let featureProj = 'EPSG:3857'
    if (parentFormat) {
      dataProj = olProjGet(parentFormat.dataProjection)
      featureProj = olProjGet(parentFormat.featureProjection || 'EPSG:3857')
    } else if (format) {
      dataProj = olProjGet(format.dataProjection)
      featureProj = olProjGet(format.featureProjection || 'EPSG:3857')
    }
    return [olProjGetProjection(dataProj), olProjGetProjection(featureProj)]
  }
  /**
   * @private
   * @method
   */
  storeFeatures() {
    this.allFeatures = this.getFeatures()
  }
}

/**
 * @public
 * @typedef {Object}
 * @property {number} distance The distance
 * @property {string} units The distance units
 */
FilterAndSort.Distance

/**
 * @desc Argument object for {@link module:nyc/ol/source/FilterAndSort~FilterAndSort#filter}
 * @public
 * @typedef {Object}
 * @property {string} property The property name on which to filter features
 * @property {Array<string>} values The values used to filter features
 */
FilterAndSort.Filter

/**
 * @desc Mixin for features
 * @private
 * @typedef {Object<string, function>}
 */
FilterAndSort.DistanceDecoration = {
  /**
   * @desc Mixin for features
   * @private
   * @method
   * @return {module:nyc/FilterAndSort~FilterAndSort.Distance} Distance
   */
  getDistance() {
    return this.get('__distance')
  }
}

export default FilterAndSort