我們來解析一下ol對於切片的計算與加載過程
創建圖層
一切的出發點都是初始化地圖:
const map = new Map({
layers: [
new TileLayer({
source: new OSM()
})
],
target: 'map',
view: new View({
center: [0, 0],
zoom: 2
})
});
new TileLayer()
時定義了渲染切片圖片的方法
創建切片數據源
new OSM()
的過程基本在各級父類中實現,創建實例並定義了很多方法,
TileImage
中定義的主要方法:
- createTile_() 創建單個切片
- getTile() 獲取單個切片
- getTileInternal() 獲取內存緩存單個切片
TileSource
中定義的主要方法:
- forEachLoadedTile() 遍歷加載切片
圖層渲染
在切片圖層渲染的renderFrame()
方法中,開始計算切片、請求、渲染
計算切片
CanvasTileLayerRenderer
renderFrame(frameState, target) {
...
// ---- 計算切片 ----
// 根據範圍和級別計算切片編號的範圍,在TileGrid中實現
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
// 嵌套循環遍歷將要加載的切片索引
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
// ---- 加載切片 ----
// 實際上嘗試是從緩存中讀取,若沒有則定義切片加載完成事件,等待切片隊列請求圖片
const tile = this.getTile(z, x, y, frameState);
if (this.isDrawableTile(tile)) {
const uid = getUid(this);
if (tile.getState() == TileState.LOADED) {
// 如果切片加載完成則在canvas上繪製切片
tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
...
}
}
...
// 通過這個方法將所需切片加入切片請求隊列中
this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
projection, extent, z, tileLayer.getPreload());
}
}
}
manageTilePyramid( ... ) {
const tileSourceKey = getUid(tileSource);
if (!(tileSourceKey in frameState.wantedTiles)) {
frameState.wantedTiles[tileSourceKey] = {};
}
const wantedTiles = frameState.wantedTiles[tileSourceKey];
//讀取隊列
const tileQueue = frameState.tileQueue;
const minZoom = tileGrid.getMinZoom();
let tile, tileRange, tileResolution, x, y, z;
// 遍歷所有需要的切片
for (z = minZoom; z <= currentZ; ++z) {
tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange);
tileResolution = tileGrid.getResolution(z);
for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
if (currentZ - z <= preload) {
tile = tileSource.getTile(z, x, y, pixelRatio, projection);
if (tile.getState() == TileState.IDLE) {
wantedTiles[tile.getKey()] = true;
if (!tileQueue.isKeyQueued(tile.getKey())) {
// 插入隊列
tileQueue.enqueue([tile, tileSourceKey,
tileGrid.getTileCoordCenter(tile.tileCoord), tileResolution]);
}
}
if (opt_tileCallback !== undefined) {
opt_tileCallback(tile);
}
} else {
tileSource.useTile(z, x, y, projection);
}
}
}
}
}
TileGrid
// 分別計算x方向和y方向上的範圍
getTileRangeForExtentAndZ(extent, z, opt_tileRange) {
const tileCoord = tmpTileCoord;
// 獲取min點的切片索引
this.getTileCoordForXYAndZ_(extent[0], extent[3], z, false, tileCoord);
const minX = tileCoord[1];
const minY = tileCoord[2];
// 獲取max點的切片索引
this.getTileCoordForXYAndZ_(extent[2], extent[1], z, true, tileCoord);
return createOrUpdateTileRange(minX, tileCoord[1], minY, tileCoord[2], opt_tileRange);
}
// 通過座標起點和當前位置的距離,除以分辨率得到像素距離,除以切片像素大小,得到索引
getTileCoordForXYAndZ_(x, y, z, reverseIntersectionPolicy, opt_tileCoord) {
// 座標起點
const origin = this.getOrigin(z);
// 當前級別下的像素分辨率
const resolution = this.getResolution(z);
// 切片圖片的像素大小
const tileSize = toSize(this.getTileSize(z), this.tmpSize_);
// 根據需要擴0.5度
const adjustX = reverseIntersectionPolicy ? 0.5 : 0;
const adjustY = reverseIntersectionPolicy ? 0.5 : 0;
// 當前位置距離起點像素距離
const xFromOrigin = Math.floor((x - origin[0]) / resolution + adjustX);
const yFromOrigin = Math.floor((origin[1] - y) / resolution + adjustY);
//除以每張切片的像素大小就是像素的位置索引了
let tileCoordX = xFromOrigin / tileSize[0];
let tileCoordY = yFromOrigin / tileSize[1];
// 切片索引取整
if (reverseIntersectionPolicy) {
tileCoordX = Math.ceil(tileCoordX) - 1;
tileCoordY = Math.ceil(tileCoordY) - 1;
} else {
tileCoordX = Math.floor(tileCoordX);
tileCoordY = Math.floor(tileCoordY);
}
// 返回索引
return createOrUpdateTileCoord(z, tileCoordX, tileCoordY, opt_tileCoord);
}
請求切片
請求切片的過程是在PluggableMap
中用隊列批量請求圖片
handlePostRender() {
const tileQueue = this.tileQueue_;
if (!tileQueue.isEmpty()) {
let maxTotalLoading = this.maxTilesLoading_;
let maxNewLoads = maxTotalLoading;
if (frameState) {
const hints = frameState.viewHints;
if (hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING]) {
const lowOnFrameBudget = !IMAGE_DECODE && Date.now() - frameState.time > 8;
maxTotalLoading = lowOnFrameBudget ? 0 : 8;
maxNewLoads = lowOnFrameBudget ? 0 : 2;
}
}
if (tileQueue.getTilesLoading() < maxTotalLoading) {
tileQueue.reprioritize(); // FIXME only call if view has changed
// 請求切片--
tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads);
}
}
if (frameState && this.hasListener(RenderEventType.RENDERCOMPLETE) && !frameState.animate &&
!this.tileQueue_.getTilesLoading() && !this.getLoading()) {
this.renderer_.dispatchRenderEvent(RenderEventType.RENDERCOMPLETE, frameState);
}
const postRenderFunctions = this.postRenderFunctions_;
for (let i = 0, ii = postRenderFunctions.length; i < ii; ++i) {
postRenderFunctions[i](this, frameState);
}
postRenderFunctions.length = 0;
}
TileQueue
loadMoreTiles(maxTotalLoading, maxNewLoads) {
let newLoads = 0;
let abortedTiles = false;
let state, tile, tileKey;
while (this.tilesLoading_ < maxTotalLoading && newLoads < maxNewLoads &&
this.getCount() > 0) {
tile = /** @type {import("./Tile.js").default} */ (this.dequeue()[0]);
tileKey = tile.getKey();
state = tile.getState();
if (state === TileState.ABORT) {
abortedTiles = true;
} else if (state === TileState.IDLE && !(tileKey in this.tilesLoadingKeys_)) {
this.tilesLoadingKeys_[tileKey] = true;
++this.tilesLoading_;
++newLoads;
// ---- 終於到load了 ---
// 在 ImageTile 中實現
tile.load();
}
}
if (newLoads === 0 && abortedTiles) {
// Do not stop the render loop when all wanted tiles were aborted due to
// a small, saturated tile cache.
this.tileChangeCallback_();
}
}
ImageTile
load() {
if (this.state == TileState.ERROR) {
this.state = TileState.IDLE;
this.image_ = new Image();
if (this.crossOrigin_ !== null) {
this.image_.crossOrigin = this.crossOrigin_;
}
}
if (this.state == TileState.IDLE) {
this.state = TileState.LOADING;
this.changed();
// 執行加載
this.tileLoadFunction_(this, this.src_);
// 綁定加載成功事件
this.unlisten_ = listenImage(
this.image_,
this.handleImageLoad_.bind(this),
this.handleImageError_.bind(this)
);
}
}
handleImageLoad_() {
// 成功加載到圖片了
const image = /** @type {HTMLImageElement} */ (this.image_);
if (image.naturalWidth && image.naturalHeight) {
this.state = TileState.LOADED;
} else {
this.state = TileState.EMPTY;
}
// 停止監聽
this.unlistenImage_();
// 派發成功事件,通知渲染
this.changed();
}