前言
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这本开源教材中提供了一种解决方案,请移步: