Openlayers源码实践系列:ol.layer.Vector矢量地图图层获取要素为空的问题原因分析及解决(基于版本6,数据使用GeoJSON)

前言

ol.layer.Vector(vectorLayer)是我们在OpenLayers开发中使用非常频繁的一个图层容器类,有时我们需要在交互中获取矢量图层中的要素(Feature)对象,但是在某些情形下,自己觉得毫无问题的代码,却无法获取到想要的数据。

本文结合源码浅析一下这个问题的原因,并提出解决的方法。

现象

先来看一段代码:


var vectorLayer = new VectorLayer({
  source: new VectorSource({
    url: "data/geojson/countries.geojson",
    format: new GeoJSON()
  })
});

var map = new Map({
  layers: [vectorLayer],
  target: "map",
  view: new View({
    center: [0, 0],
    zoom: 1
  })
});

console.log(vectorLayer.getSource().getFeatures());

这是一段来自OpenLayers官方实例的很简单的代码,在代码的最后一行,我使用了getFeatures()方法来获取VectorSource对象中的所有要素,期待的输出应该是在控制台输出一个Feature对象的数组。

然而实际运行的结果是:

得到的是一个空数组。那么程序运行的时候是没有得到数据吗?看一下渲染的地图:

完全正确。

那么问题出在哪呢? 

分析

乍一看上面的代码逻辑无懈可击,但是稍有JavaScript编程经验的就会开始怀疑一个地方:

  source: new VectorSource({
    url: "data/geojson/countries.geojson",
    format: new GeoJSON()
  })

在我们初始化VectorSource的时候,是通过指定了一个定位到GeoJSON标准文件的url链接来为其配置数据源的,那么这里很可能就是用Ajax来将数据读出来,再转化成OpenLayers的Feature对象。

下面来看一下这里相关的源码实现。

在VectorSource的源码文件中发现了这样一段话:

然后在下面找到了私有成员url的处理过程:

  /**
     * @private
     * @type {string|import("../featureloader.js").FeatureUrlFunction|undefined}
     */
    this.url_ = options.url;

    if (options.loader !== undefined) {
      this.loader_ = options.loader;
    } else if (this.url_ !== undefined) {
      assert(this.format_, 7); // `format` must be set when `url` is set
      // create a XHR feature loader for "url" and "format"
      this.loader_ = xhr(this.url_, /** @type {import("../format/Feature.js").default} */ (this.format_));
    }

这段代码的意思是,如果初始化的时候定义了url,则将loader初始化为一个默认的加载器来加载url,那么这个xhr从哪来的?

继续向上找:

import {xhr} from '../featureloader.js';

好的,再去看看它。

 xhr这个函数的声明如下:

/**
 * Create an XHR feature loader for a `url` and `format`. The feature loader
 * loads features (with XHR), parses the features, and adds them to the
 * vector source.
 * @param {string|FeatureUrlFunction} url Feature URL service.
 * @param {import("./format/Feature.js").default} format Feature format.
 * @return {FeatureLoader} The feature loader.
 * @api
 */
export function xhr(url, format) {
  return loadFeaturesXhr(url, format,
    /**
     * @param {Array<import("./Feature.js").default>} features The loaded features.
     * @param {import("./proj/Projection.js").default} dataProjection Data
     * projection.
     * @this {import("./source/Vector").default|import("./VectorTile.js").default}
     */
    function(features, dataProjection) {
      const sourceOrTile = /** @type {?} */ (this);
      if (typeof sourceOrTile.addFeatures === 'function') {
        /** @type {import("./source/Vector").default} */ (sourceOrTile).addFeatures(features);
      }
    }, /* FIXME handle error */ VOID);
}

 原来它是调用了一个内部函数loadFeaturesXhr,继续看:

/**
 * @param {string|FeatureUrlFunction} url Feature URL service.
 * @param {import("./format/Feature.js").default} format Feature format.
 * @param {function(this:import("./VectorTile.js").default, Array<import("./Feature.js").default>, import("./proj/Projection.js").default, import("./extent.js").Extent): void|function(this:import("./source/Vector").default, Array<import("./Feature.js").default>): void} success
 *     Function called with the loaded features and optionally with the data
 *     projection. Called with the vector tile or source as `this`.
 * @param {function(this:import("./VectorTile.js").default): void|function(this:import("./source/Vector").default): void} failure
 *     Function called when loading failed. Called with the vector tile or
 *     source as `this`.
 * @return {FeatureLoader} The feature loader.
 */
export function loadFeaturesXhr(url, format, success, failure) {
  return (
    /**
     * @param {import("./extent.js").Extent} extent Extent.
     * @param {number} resolution Resolution.
     * @param {import("./proj/Projection.js").default} projection Projection.
     * @this {import("./source/Vector").default|import("./VectorTile.js").default}
     */
    function(extent, resolution, projection) {
      const xhr = new XMLHttpRequest();
      xhr.open('GET',
        typeof url === 'function' ? url(extent, resolution, projection) : url,
        true);
      if (format.getType() == FormatType.ARRAY_BUFFER) {
        xhr.responseType = 'arraybuffer';
      }
      xhr.withCredentials = withCredentials;
      /**
       * @param {Event} event Event.
       * @private
       */
      xhr.onload = function(event) {
        // status will be 0 for file:// urls
        if (!xhr.status || xhr.status >= 200 && xhr.status < 300) {
          const type = format.getType();
          /** @type {Document|Node|Object|string|undefined} */
          let source;
          if (type == FormatType.JSON || type == FormatType.TEXT) {
            source = xhr.responseText;
          } else if (type == FormatType.XML) {
            source = xhr.responseXML;
            if (!source) {
              source = new DOMParser().parseFromString(xhr.responseText, 'application/xml');
            }
          } else if (type == FormatType.ARRAY_BUFFER) {
            source = /** @type {ArrayBuffer} */ (xhr.response);
          }
          if (source) {
            success.call(this, format.readFeatures(source, {
              extent: extent,
              featureProjection: projection
            }),
            format.readProjection(source));
          } else {
            failure.call(this);
          }
        } else {
          failure.call(this);
        }
      }.bind(this);
      /**
       * @private
       */
      xhr.onerror = function() {
        failure.call(this);
      }.bind(this);
      xhr.send();
    }
  );
}

OK,其实到这里就可以看出来了,这是一个非常规整的原生Ajax调用过程。

 那么看到这里,我们上面的猜测就得到验证:

因为Feature数据是通过Ajax加载的,而getFeatures()调用与这个过程是异步执行的,并未等到数据完全加载就提前调用了,所以我们无法按照预期获得要素对象数组。

在上面的代码中可以看出,当loadFeaturesXhr返回时,Ajax调用一定是完成的,也就是在此时一定可以拿到source中的要素。那么接下来看一下哪里调用了这个loader,看看在这个时机附近是否有事件触发,如果有的话,我们就可以通过监听这个事件来拿到要素数组了。

下面是VectorSource类的私有成员函数loadFeatures,它调用了默认的loader。

  /**
   * @param {import("../extent.js").Extent} extent Extent.
   * @param {number} resolution Resolution.
   * @param {import("../proj/Projection.js").default} projection Projection.
   */
  loadFeatures(extent, resolution, projection) {
    const loadedExtentsRtree = this.loadedExtentsRtree_;
    const extentsToLoad = this.strategy_(extent, resolution);
    this.loading = false;
    for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) {
      const extentToLoad = extentsToLoad[i];
      const alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad,
        /**
         * @param {{extent: import("../extent.js").Extent}} object Object.
         * @return {boolean} Contains.
         */
        function(object) {
          return containsExtent(object.extent, extentToLoad);
        });
      if (!alreadyLoaded) {
        this.loader_.call(this, extentToLoad, resolution, projection);
        loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()});
        this.loading = this.loader_ !== VOID;
      }
    }
  }

然后再看看哪里调用了这个loadFeatures,于是又顺藤摸瓜摸到了VectorLayer的渲染器CanvasVectorLayerRenderer:

 /**
   * @inheritDoc
   */
  prepareFrame(frameState) {
……
const replayGroup = new CanvasBuilderGroup(
      getRenderTolerance(resolution, pixelRatio), extent, resolution,
      pixelRatio, vectorLayer.getDeclutter());

    const userProjection = getUserProjection();
    let userTransform;
    if (userProjection) {
      for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
        vectorSource.loadFeatures(toUserExtent(loadExtents[i], projection), resolution, userProjection);
      }
      userTransform = getTransformFromProjections(userProjection, projection);
    } else {
      for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
        vectorSource.loadFeatures(loadExtents[i], resolution, projection);
      }
    }
……
}

由之前的文章:

探索layer的渲染机制——从分析OpenLayers 6 的WebGLPointsLayer动画效果实现说起

中我们可以获知,这个prepareFrame是在renderFrame之前执行的,它执行之后紧接着renderFrame开始的时候,会触发一个prerender事件。

OK,走到这里,我们已经彻底搞清了拿不到要素数组背后的原因,并且已经有了解决思路。

解决

思路很简单,就是在需要立即拿到要素数组的地方,通过绑定VectorLayer的prerender事件,在执行回调函数中调用getFeatures()。另外,为了实现一次性顺序加载的效果,应该使用once而不是on,并且,所有依赖这一步操作的代码,必须都要写到这个回调函数里,以保证操作的同步性:

var vectorLayer = new VectorLayer({
  source: new VectorSource({
    url: "data/geojson/countries.geojson",
    format: new GeoJSON()
  })
});

var map = new Map({
  layers: [vectorLayer],
  target: "map",
  view: new View({
    center: [0, 0],
    zoom: 1
  })
});

console.log("立即执行结果:");
console.log(vectorLayer.getSource().getFeatures());

vectorLayer.once("prerender", evt => {
  console.log("渲染前执行结果:");
  console.log(vectorLayer.getSource().getFeatures());
});

运行效果如下:

另外:在OpenLayers Primer这本开源教材中提供了一种解决方案,请移步:

获取加载后的所有feature

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战斗中的老胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值