text: import debounce from 'debounce-promise'
  import { Feature } from 'ol'
  import ObjectEventType from 'ol/ObjectEventType'
  import { map as mapObs, skipWhile } from 'rxjs/operators'
  import { getFeatureId, getFeatureProperties, initializeFeature, setFeatureId, setFeatureProperties } from '../ol-ext'
  import {
    fromOlEvent as obsFromOlEvent,
    fromVueEvent as obsFromVueEvent,
    fromVueWatcher as obsFromVueWatcher,
  } from '../rx-ext'
  import { assert, clonePlainObject, hasProp, isEqual, pick, stubObject, mergeDescriptors, waitFor } from '../utils'
  import geometryContainer from './geometry-container'
  import olCmp, { FRAME_TIME, OlObjectEvent } from './ol-cmp'
  import projTransforms from './proj-transforms'
  import stubVNode from './stub-vnode'
  import styleContainer from './style-container'
  
  /**
   * A vector object for geographic features with a geometry and other attribute properties,
   * similar to the features in vector file formats like **GeoJSON**.
   */
  export default {
    mixins: [
      stubVNode,
      projTransforms,
      geometryContainer,
      styleContainer,
      olCmp,
    ],
    stubVNode: {
      empty: false,
      attrs () {
        return {
          id: this.vmId,
          class: this.vmClass,
        }
      },
    },
    props: {
      properties: {
        type: Object,
        default: stubObject,
      },
    },
    data () {
      return {
        dataProjection: null,
      }
    },
    computed: {
      currentProperties () {
        if (this.rev && this.$feature) {
          return this.getPropertiesInternal()
        }
  
        return this.properties
      },
      currentGeometryName () {
        if (!(this.rev && this.$feature)) return
  
        return this.getGeometryNameInternal()
      },
    },
    watch: {
      properties: {
        deep: true,
        async handler (value) {
          await this.setProperties(value)
        },
      },
      currentProperties: {
        deep: true,
        handler: /*#__PURE__*/debounce(function (value) {
          if (isEqual(value, this.properties)) return
  
          this.$emit('update:properties', clonePlainObject(value))
        }, FRAME_TIME),
      },
      currentGeometryName: /*#__PURE__*/debounce(function (value, prev) {
        if (value === prev) return
  
        this.$emit('update:geometryName', value)
      }, FRAME_TIME),
    },
    created () {
      this::defineServices()
    },
    methods: {
      /**
       * @return {Promise<void>}
       * @protected
       */
      async beforeInit () {
        try {
          await waitFor(
            () => this.$mapVm != null,
            obsFromVueEvent(this.$eventBus, [
              OlObjectEvent.CREATE_ERROR,
            ]).pipe(
              mapObs(([vm]) => hasProp(vm, '$map') && this.$vq.closest(vm)),
            ),
            1000,
          )
          this.dataProjection = this.$mapVm.resolvedDataProjection
          const dataProjChanges = obsFromVueWatcher(this, () => this.$mapVm.resolvedDataProjection)
          this.subscribeTo(dataProjChanges, ({ value }) => { this.dataProjection = value })
          await this.$nextTickPromise()
  
          return this::olCmp.methods.beforeInit()
        } catch (err) {
          err.message = 'Wait for $mapVm injection: ' + err.message
          throw err
        }
      },
      /**
       * Create feature without inner style applying, feature level style
       * will be applied in the layer level style function.
       * @return {module:ol/Feature~Feature}
       * @protected
       */
      createOlObject () {
        const feature = initializeFeature(this.createFeature(), this.currentId)
        feature.setGeometry(this.$geometry)
  
        return feature
      },
      /**
       * @returns {Feature}
       */
      createFeature () {
        return new Feature(this.properties)
      },
      /**
       * @return {Promise<void>}
       * @protected
       */
      async beforeMount () {
        try {
          await waitFor(
            () => this.$geometryVm != null,
            obsFromVueEvent(this.$eventBus, [
              ObjectEventType.CREATE_ERROR,
              ObjectEventType.MOUNT_ERROR,
            ]).pipe(
              mapObs(([vm]) => hasProp(vm, '$geometry') && this.$vq.find(vm).length),
            ),
            1000,
          )
  
          return this::olCmp.methods.beforeMount()
        } catch (err) {
          err.message = 'Wait for $geometry failed: ' + err.message
          throw err
        }
      },
      /**
       * @return {Promise<void>}
       * @protected
       */
      async mount () {
        if (this.$featuresContainer) {
          await this.$featuresContainer.addFeature(this)
        }
  
        return this::olCmp.methods.mount()
      },
      /**
       * @return {Promise<void>}
       * @protected
       */
      async unmount () {
        if (this.$featuresContainer) {
          await this.$featuresContainer.removeFeature(this)
        }
  
        return this::olCmp.methods.unmount()
      },
      /**
       * @return {void}
       * @protected
       */
      subscribeAll () {
        this::olCmp.methods.subscribeAll()
        this::subscribeToEvents()
      },
      /**
       * @return {Object}
       * @protected
       */
      getServices () {
        const vm = this
  
        return mergeDescriptors(
          this::olCmp.methods.getServices(),
          this::geometryContainer.methods.getServices(),
          this::styleContainer.methods.getServices(),
          {
            get featureVm () { return vm },
          },
        )
      },
      .../*#__PURE__*/pick(olCmp.methods, [
        'refresh',
        'scheduleRefresh',
        'remount',
        'scheduleRemount',
        'recreate',
        'scheduleRecreate',
        'resolveOlObject',
      ]),
      .../*#__PURE__*/pick(geometryContainer.methods, [
        'setGeometry',
      ]),
      .../*#__PURE__*/pick(styleContainer.methods, [
        'setStyle',
      ]),
      resolveFeature: olCmp.methods.resolveOlObject,
      getGeometryTarget: olCmp.methods.resolveOlObject,
      getStyleTarget: olCmp.methods.resolveOlObject,
      /**
       * @return {string|number}
       */
      getIdInternal () {
        return getFeatureId(this.$feature)
      },
      /**
       * @param {string|number} id
       * @return {void}
       */
      setIdInternal (id) {
        assert(id != null && id !== '', 'Invalid feature id')
  
        if (id === this.getIdInternal()) return
  
        setFeatureId(this.$feature, id)
      },
      /**
       * @return {Promise<string>}
       */
      async getGeometryName () {
        await this.resolveFeature()
  
        return this.getGeometryNameInternal()
      },
      /**
       * @return {string}
       * @protected
       */
      getGeometryNameInternal () {
        return this.$feature.getGeometryName()
      },
      /**
       * @param {string} geometryName
       * @return {Promise<void>}
       */
      async setGeometryName (geometryName) {
        if (geometryName === await this.getGeometryName()) return
  
        (await this.resolveFeature()).setGeometryName(geometryName)
      },
      /**
       * @return {Promise<Object>}
       */
      async getProperties () {
        await this.resolveFeature()
  
        return this.getPropertiesInternal()
      },
      getPropertiesInternal () {
        return getFeatureProperties(this.$feature)
      },
      /**
       * @param {Object} properties
       * @return {Promise<void>}
       */
      async setProperties (properties) {
        properties = getFeatureProperties({ properties })
        if (isEqual(properties, await this.getProperties())) return
  
        setFeatureProperties(await this.resolveFeature(), properties)
      },
      /**
       * Checks if feature lies at `pixel`.
       * @param {number[]} pixel
       * @return {Promise<boolean>}
       */
      async isAtPixel (pixel) {
        const selfFeature = await this.resolveFeature()
        let layerFilter
        if (this.$layerVm) {
          const selfLayer = await this.$layerVm.resolveLayer()
          layerFilter = layer => layer === selfLayer
        }
  
        return this.$mapVm.forEachFeatureAtPixel(pixel, feature => feature === selfFeature, { layerFilter })
      },
    },
  }
  
  function defineServices () {
    Object.defineProperties(this, {
      $feature: {
        enumerable: true,
        get: () => this.$olObject,
      },
      $layerVm: {
        enumerable: true,
        get: () => this.$services?.layerVm,
      },
      $mapVm: {
        enumerable: true,
        get: () => this.$services?.mapVm,
      },
      $featuresContainer: {
        enumerable: true,
        get: () => this.$services?.featuresContainer,
      },
    })
  }
  
  async function subscribeToEvents () {
    const propChanges = obsFromOlEvent(
      this.$feature,
      ObjectEventType.PROPERTYCHANGE,
      ({ key }) => {
        switch (key) {
          case this.$feature.getGeometryName():
            return {
              prop: 'geometry',
              value: this.writeGeometryInDataProj(this.getGeometry()),
              compareWith: this.currentGeometryDataProj,
            }
          default:
            return {
              prop: 'properties',
              value: {
                ...this.properties,
                [key]: this.$feature.get(key),
              },
              compareWith: this.currentProperties,
            }
        }
      },
    ).pipe(
      skipWhile(({ value, compareWith }) => isEqual(value, compareWith)),
    )
    this.subscribeTo(propChanges, () => {
      ++this.rev
    })
  }
  