赞
踩
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这本开源教材中提供了一种解决方案,请移步:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。