当前位置:   article > 正文

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

openlayer面图层getfeature为空

前言

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

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

现象

先来看一段代码:

  1. var vectorLayer = new VectorLayer({
  2. source: new VectorSource({
  3. url: "data/geojson/countries.geojson",
  4. format: new GeoJSON()
  5. })
  6. });
  7. var map = new Map({
  8. layers: [vectorLayer],
  9. target: "map",
  10. view: new View({
  11. center: [0, 0],
  12. zoom: 1
  13. })
  14. });
  15. console.log(vectorLayer.getSource().getFeatures());

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

然而实际运行的结果是:

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

完全正确。

那么问题出在哪呢? 

分析

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

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

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

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

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

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

  1. /**
  2. * @private
  3. * @type {string|import("../featureloader.js").FeatureUrlFunction|undefined}
  4. */
  5. this.url_ = options.url;
  6. if (options.loader !== undefined) {
  7. this.loader_ = options.loader;
  8. } else if (this.url_ !== undefined) {
  9. assert(this.format_, 7); // `format` must be set when `url` is set
  10. // create a XHR feature loader for "url" and "format"
  11. this.loader_ = xhr(this.url_, /** @type {import("../format/Feature.js").default} */ (this.format_));
  12. }

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

继续向上找:

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

好的,再去看看它。

 xhr这个函数的声明如下:

  1. /**
  2. * Create an XHR feature loader for a `url` and `format`. The feature loader
  3. * loads features (with XHR), parses the features, and adds them to the
  4. * vector source.
  5. * @param {string|FeatureUrlFunction} url Feature URL service.
  6. * @param {import("./format/Feature.js").default} format Feature format.
  7. * @return {FeatureLoader} The feature loader.
  8. * @api
  9. */
  10. export function xhr(url, format) {
  11. return loadFeaturesXhr(url, format,
  12. /**
  13. * @param {Array<import("./Feature.js").default>} features The loaded features.
  14. * @param {import("./proj/Projection.js").default} dataProjection Data
  15. * projection.
  16. * @this {import("./source/Vector").default|import("./VectorTile.js").default}
  17. */
  18. function(features, dataProjection) {
  19. const sourceOrTile = /** @type {?} */ (this);
  20. if (typeof sourceOrTile.addFeatures === 'function') {
  21. /** @type {import("./source/Vector").default} */ (sourceOrTile).addFeatures(features);
  22. }
  23. }, /* FIXME handle error */ VOID);
  24. }

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

  1. /**
  2. * @param {string|FeatureUrlFunction} url Feature URL service.
  3. * @param {import("./format/Feature.js").default} format Feature format.
  4. * @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
  5. * Function called with the loaded features and optionally with the data
  6. * projection. Called with the vector tile or source as `this`.
  7. * @param {function(this:import("./VectorTile.js").default): void|function(this:import("./source/Vector").default): void} failure
  8. * Function called when loading failed. Called with the vector tile or
  9. * source as `this`.
  10. * @return {FeatureLoader} The feature loader.
  11. */
  12. export function loadFeaturesXhr(url, format, success, failure) {
  13. return (
  14. /**
  15. * @param {import("./extent.js").Extent} extent Extent.
  16. * @param {number} resolution Resolution.
  17. * @param {import("./proj/Projection.js").default} projection Projection.
  18. * @this {import("./source/Vector").default|import("./VectorTile.js").default}
  19. */
  20. function(extent, resolution, projection) {
  21. const xhr = new XMLHttpRequest();
  22. xhr.open('GET',
  23. typeof url === 'function' ? url(extent, resolution, projection) : url,
  24. true);
  25. if (format.getType() == FormatType.ARRAY_BUFFER) {
  26. xhr.responseType = 'arraybuffer';
  27. }
  28. xhr.withCredentials = withCredentials;
  29. /**
  30. * @param {Event} event Event.
  31. * @private
  32. */
  33. xhr.onload = function(event) {
  34. // status will be 0 for file:// urls
  35. if (!xhr.status || xhr.status >= 200 && xhr.status < 300) {
  36. const type = format.getType();
  37. /** @type {Document|Node|Object|string|undefined} */
  38. let source;
  39. if (type == FormatType.JSON || type == FormatType.TEXT) {
  40. source = xhr.responseText;
  41. } else if (type == FormatType.XML) {
  42. source = xhr.responseXML;
  43. if (!source) {
  44. source = new DOMParser().parseFromString(xhr.responseText, 'application/xml');
  45. }
  46. } else if (type == FormatType.ARRAY_BUFFER) {
  47. source = /** @type {ArrayBuffer} */ (xhr.response);
  48. }
  49. if (source) {
  50. success.call(this, format.readFeatures(source, {
  51. extent: extent,
  52. featureProjection: projection
  53. }),
  54. format.readProjection(source));
  55. } else {
  56. failure.call(this);
  57. }
  58. } else {
  59. failure.call(this);
  60. }
  61. }.bind(this);
  62. /**
  63. * @private
  64. */
  65. xhr.onerror = function() {
  66. failure.call(this);
  67. }.bind(this);
  68. xhr.send();
  69. }
  70. );
  71. }

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

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

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

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

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

  1. /**
  2. * @param {import("../extent.js").Extent} extent Extent.
  3. * @param {number} resolution Resolution.
  4. * @param {import("../proj/Projection.js").default} projection Projection.
  5. */
  6. loadFeatures(extent, resolution, projection) {
  7. const loadedExtentsRtree = this.loadedExtentsRtree_;
  8. const extentsToLoad = this.strategy_(extent, resolution);
  9. this.loading = false;
  10. for (let i = 0, ii = extentsToLoad.length; i < ii; ++i) {
  11. const extentToLoad = extentsToLoad[i];
  12. const alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad,
  13. /**
  14. * @param {{extent: import("../extent.js").Extent}} object Object.
  15. * @return {boolean} Contains.
  16. */
  17. function(object) {
  18. return containsExtent(object.extent, extentToLoad);
  19. });
  20. if (!alreadyLoaded) {
  21. this.loader_.call(this, extentToLoad, resolution, projection);
  22. loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()});
  23. this.loading = this.loader_ !== VOID;
  24. }
  25. }
  26. }

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

  1. /**
  2. * @inheritDoc
  3. */
  4. prepareFrame(frameState) {
  5. ……
  6. const replayGroup = new CanvasBuilderGroup(
  7. getRenderTolerance(resolution, pixelRatio), extent, resolution,
  8. pixelRatio, vectorLayer.getDeclutter());
  9. const userProjection = getUserProjection();
  10. let userTransform;
  11. if (userProjection) {
  12. for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
  13. vectorSource.loadFeatures(toUserExtent(loadExtents[i], projection), resolution, userProjection);
  14. }
  15. userTransform = getTransformFromProjections(userProjection, projection);
  16. } else {
  17. for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
  18. vectorSource.loadFeatures(loadExtents[i], resolution, projection);
  19. }
  20. }
  21. ……
  22. }

由之前的文章:

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

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

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

解决

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

  1. var vectorLayer = new VectorLayer({
  2. source: new VectorSource({
  3. url: "data/geojson/countries.geojson",
  4. format: new GeoJSON()
  5. })
  6. });
  7. var map = new Map({
  8. layers: [vectorLayer],
  9. target: "map",
  10. view: new View({
  11. center: [0, 0],
  12. zoom: 1
  13. })
  14. });
  15. console.log("立即执行结果:");
  16. console.log(vectorLayer.getSource().getFeatures());
  17. vectorLayer.once("prerender", evt => {
  18. console.log("渲染前执行结果:");
  19. console.log(vectorLayer.getSource().getFeatures());
  20. });

运行效果如下:

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

获取加载后的所有feature

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/155770
推荐阅读
相关标签
  

闽ICP备14008679号