第09章 - 栅格数据与瓦片服务
第09章 - 栅格数据与瓦片服务
9.1 瓦片原理
9.1.1 瓦片金字塔
瓦片金字塔是一种空间数据组织方式,将地图按照不同缩放级别切分成固定大小的图片块(瓦片)。
┌─────────────────────────────────────────────────────────────┐
│ 瓦片金字塔结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level 0 (zoom=0) ┌─────┐ │
│ 1 tile │ │ │
│ └─────┘ │
│ │
│ Level 1 (zoom=1) ┌──┬──┐ │
│ 4 tiles │ │ │ │
│ ├──┼──┤ │
│ │ │ │ │
│ └──┴──┘ │
│ │
│ Level 2 (zoom=2) ┌─┬─┬─┬─┐ │
│ 16 tiles │ │ │ │ │ │
│ ├─┼─┼─┼─┤ │
│ │ │ │ │ │ │
│ ├─┼─┼─┼─┤ │
│ │ │ │ │ │ │
│ ├─┼─┼─┼─┤ │
│ │ │ │ │ │ │
│ └─┴─┴─┴─┘ │
│ │
│ 瓦片数量 = 4^zoom │
│ 分辨率 = 初始分辨率 / 2^zoom │
│ │
└─────────────────────────────────────────────────────────────┘
9.1.2 瓦片编号方案
// XYZ 编号方案(最常用)
// z: 缩放级别
// x: 从左到右,0 开始
// y: 从上到下,0 开始(TMS 是从下到上)
const url = `https://tile.openstreetmap.org/${z}/${x}/${y}.png`;
// TMS 编号方案
// y 轴方向相反
const tmsY = Math.pow(2, z) - 1 - y;
const tmsUrl = `https://example.com/${z}/${x}/${tmsY}.png`;
// QuadKey 编号方案(Bing Maps)
function tileXYToQuadKey(x, y, z) {
let quadKey = '';
for (let i = z; i > 0; i--) {
let digit = 0;
const mask = 1 << (i - 1);
if ((x & mask) !== 0) digit++;
if ((y & mask) !== 0) digit += 2;
quadKey += digit.toString();
}
return quadKey;
}
9.1.3 分辨率计算
// Web Mercator 投影下的分辨率计算
const EARTH_CIRCUMFERENCE = 40075016.685578488; // 米
const TILE_SIZE = 256; // 像素
// 计算指定缩放级别的分辨率(米/像素)
function getResolution(zoom) {
return EARTH_CIRCUMFERENCE / (TILE_SIZE * Math.pow(2, zoom));
}
// 常用缩放级别对应的分辨率
const resolutions = [];
for (let z = 0; z <= 22; z++) {
resolutions[z] = getResolution(z);
}
// Level 0: 156543.03 米/像素
// Level 10: 152.87 米/像素
// Level 18: 0.60 米/像素
// 根据分辨率计算缩放级别
function getZoom(resolution) {
return Math.log2(EARTH_CIRCUMFERENCE / (TILE_SIZE * resolution));
}
9.2 瓦片网格
9.2.1 创建瓦片网格
import TileGrid from 'ol/tilegrid/TileGrid';
import { get as getProjection, getWidth, getTopLeft } from 'ol/proj';
import { getWidth as getExtentWidth, getTopLeft as getExtentTopLeft } from 'ol/extent';
// 默认瓦片网格
const projection = getProjection('EPSG:3857');
const projectionExtent = projection.getExtent();
const size = getExtentWidth(projectionExtent) / 256;
const resolutions = [];
const matrixIds = [];
for (let z = 0; z < 19; z++) {
resolutions[z] = size / Math.pow(2, z);
matrixIds[z] = z;
}
// 创建自定义瓦片网格
const tileGrid = new TileGrid({
// 瓦片原点(通常为左上角)
origin: getExtentTopLeft(projectionExtent),
// 分辨率数组
resolutions: resolutions,
// 瓦片大小
tileSize: [256, 256],
// 范围
extent: projectionExtent
});
// 使用瓦片网格
const tileLayer = new TileLayer({
source: new XYZ({
url: 'https://example.com/tiles/{z}/{x}/{y}.png',
tileGrid: tileGrid
})
});
9.2.2 WMTS 瓦片网格
import WMTSTileGrid from 'ol/tilegrid/WMTS';
import WMTS from 'ol/source/WMTS';
// WMTS 瓦片网格
const wmtsTileGrid = new WMTSTileGrid({
origin: getExtentTopLeft(projectionExtent),
resolutions: resolutions,
matrixIds: matrixIds.map(id => id.toString())
});
// 使用 WMTS 瓦片网格
const wmtsSource = new WMTS({
url: 'http://example.com/wmts',
layer: 'layer_name',
matrixSet: 'EPSG:3857',
format: 'image/png',
tileGrid: wmtsTileGrid,
style: 'default'
});
9.2.3 自定义瓦片网格
// 创建 4490 投影的瓦片网格
import { register } from 'ol/proj/proj4';
import proj4 from 'proj4';
proj4.defs('EPSG:4490', '+proj=longlat +ellps=GRS80 +no_defs');
register(proj4);
const cgcs2000Extent = [-180, -90, 180, 90];
const cgcs2000Resolutions = [];
for (let z = 0; z < 21; z++) {
cgcs2000Resolutions[z] = 180 / (256 * Math.pow(2, z));
}
const cgcs2000TileGrid = new TileGrid({
origin: [-180, 90],
resolutions: cgcs2000Resolutions,
tileSize: [256, 256],
extent: cgcs2000Extent
});
9.3 瓦片加载与缓存
9.3.1 自定义瓦片加载
import XYZ from 'ol/source/XYZ';
// 带认证的瓦片加载
const authenticatedSource = new XYZ({
url: 'https://secure.example.com/tiles/{z}/{x}/{y}.png',
tileLoadFunction: function(imageTile, src) {
const xhr = new XMLHttpRequest();
xhr.open('GET', src);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.responseType = 'blob';
xhr.onload = function() {
if (xhr.status === 200) {
const blob = xhr.response;
const imageUrl = URL.createObjectURL(blob);
imageTile.getImage().src = imageUrl;
// 清理 blob URL
imageTile.getImage().onload = () => {
URL.revokeObjectURL(imageUrl);
};
}
};
xhr.onerror = function() {
console.error('瓦片加载失败:', src);
};
xhr.send();
}
});
// 带重试的瓦片加载
function createRetryTileLoadFunction(maxRetries = 3) {
return function(imageTile, src) {
let retries = 0;
function load() {
const image = imageTile.getImage();
image.onerror = function() {
if (retries < maxRetries) {
retries++;
console.log(`重试 ${retries}/${maxRetries}: ${src}`);
setTimeout(load, 1000 * retries);
}
};
image.src = src;
}
load();
};
}
9.3.2 离线瓦片缓存
// 使用 IndexedDB 缓存瓦片
class TileCache {
constructor(dbName = 'tile-cache') {
this.dbName = dbName;
this.db = null;
this.initDB();
}
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('tiles')) {
db.createObjectStore('tiles', { keyPath: 'key' });
}
};
});
}
async get(key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['tiles'], 'readonly');
const store = transaction.objectStore('tiles');
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result?.data);
});
}
async put(key, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['tiles'], 'readwrite');
const store = transaction.objectStore('tiles');
const request = store.put({ key, data, timestamp: Date.now() });
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async clear() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['tiles'], 'readwrite');
const store = transaction.objectStore('tiles');
const request = store.clear();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
}
// 创建带缓存的瓦片加载函数
async function createCachedTileLoadFunction(cache) {
await cache.initDB();
return async function(imageTile, src) {
const key = src;
// 尝试从缓存读取
const cached = await cache.get(key);
if (cached) {
imageTile.getImage().src = cached;
return;
}
// 从网络加载
try {
const response = await fetch(src);
const blob = await response.blob();
const dataUrl = await new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
// 保存到缓存
await cache.put(key, dataUrl);
imageTile.getImage().src = dataUrl;
} catch (error) {
console.error('瓦片加载失败:', error);
}
};
}
9.3.3 预加载瓦片
// 预加载指定范围的瓦片
async function preloadTiles(source, extent, zoomLevels) {
const tileGrid = source.getTileGrid();
const projection = source.getProjection();
for (const zoom of zoomLevels) {
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom);
for (let x = tileRange.minX; x <= tileRange.maxX; x++) {
for (let y = tileRange.minY; y <= tileRange.maxY; y++) {
const tileCoord = [zoom, x, y];
const tileUrl = source.getTileUrlFunction()(tileCoord, 1, projection);
if (tileUrl) {
// 预加载图片
const img = new Image();
img.src = tileUrl;
await new Promise((resolve) => {
img.onload = resolve;
img.onerror = resolve;
});
}
}
}
}
console.log('预加载完成');
}
// 使用示例
const extent = map.getView().calculateExtent(map.getSize());
preloadTiles(tileSource, extent, [10, 11, 12]);
9.4 WMS 服务
9.4.1 WMS 瓦片
import TileWMS from 'ol/source/TileWMS';
import TileLayer from 'ol/layer/Tile';
const wmsLayer = new TileLayer({
source: new TileWMS({
url: 'http://localhost:8080/geoserver/wms',
params: {
'LAYERS': 'workspace:layer',
'TILED': true,
'FORMAT': 'image/png',
'TRANSPARENT': true,
'STYLES': '',
'VERSION': '1.1.1'
},
serverType: 'geoserver',
crossOrigin: 'anonymous',
// 瓦片配置
tileGrid: tileGrid,
gutter: 0
})
});
// 动态更新 WMS 参数
function updateWMSFilter(layer, filter) {
const source = layer.getSource();
source.updateParams({
'CQL_FILTER': filter
});
}
// 获取图例
function getWMSLegend(source, layerName) {
const params = source.getParams();
const baseUrl = source.getUrl();
return `${baseUrl}?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetLegendGraphic` +
`&LAYER=${layerName}&FORMAT=image/png`;
}
9.4.2 WMS 图像
import ImageWMS from 'ol/source/ImageWMS';
import ImageLayer from 'ol/layer/Image';
const imageWMSLayer = new ImageLayer({
source: new ImageWMS({
url: 'http://localhost:8080/geoserver/wms',
params: {
'LAYERS': 'workspace:layer',
'FORMAT': 'image/png',
'TRANSPARENT': true
},
serverType: 'geoserver',
ratio: 1.5, // 请求比视口大 50% 的图像
crossOrigin: 'anonymous'
})
});
// GetFeatureInfo 查询
map.on('singleclick', async (event) => {
const source = imageWMSLayer.getSource();
const url = source.getFeatureInfoUrl(
event.coordinate,
map.getView().getResolution(),
'EPSG:3857',
{
'INFO_FORMAT': 'application/json',
'FEATURE_COUNT': 10
}
);
if (url) {
const response = await fetch(url);
const data = await response.json();
console.log('查询结果:', data);
}
});
9.5 WMTS 服务
9.5.1 WMTS 配置
import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS';
import WMTSCapabilities from 'ol/format/WMTSCapabilities';
import TileLayer from 'ol/layer/Tile';
// 从 Capabilities 自动配置
async function createWMTSLayerFromCapabilities(url, layerName) {
const response = await fetch(`${url}?SERVICE=WMTS&REQUEST=GetCapabilities`);
const text = await response.text();
const parser = new WMTSCapabilities();
const capabilities = parser.read(text);
const options = optionsFromCapabilities(capabilities, {
layer: layerName,
matrixSet: 'EPSG:3857'
});
return new TileLayer({
source: new WMTS(options)
});
}
// 手动配置 WMTS
const wmtsLayer = new TileLayer({
source: new WMTS({
url: 'http://localhost:8080/geoserver/gwc/service/wmts',
layer: 'workspace:layer',
matrixSet: 'EPSG:3857',
format: 'image/png',
projection: 'EPSG:3857',
tileGrid: wmtsTileGrid,
style: 'default',
wrapX: true
})
});
9.5.2 天地图 WMTS
function createTiandituWMTS(layerType, key) {
const projection = getProjection('EPSG:3857');
const projectionExtent = projection.getExtent();
const size = getWidth(projectionExtent) / 256;
const resolutions = [];
const matrixIds = [];
for (let z = 1; z < 19; z++) {
resolutions.push(size / Math.pow(2, z));
matrixIds.push(z.toString());
}
const layers = {
vec: { layer: 'vec', format: 'tiles' },
cva: { layer: 'cva', format: 'tiles' },
img: { layer: 'img', format: 'tiles' },
cia: { layer: 'cia', format: 'tiles' },
ter: { layer: 'ter', format: 'tiles' },
cta: { layer: 'cta', format: 'tiles' }
};
const config = layers[layerType];
return new TileLayer({
source: new WMTS({
url: `http://t{0-7}.tianditu.gov.cn/${config.layer}_w/wmts?tk=${key}`,
layer: config.layer,
matrixSet: 'w',
format: config.format,
projection: projection,
tileGrid: new WMTSTileGrid({
origin: getTopLeft(projectionExtent),
resolutions: resolutions,
matrixIds: matrixIds
}),
style: 'default',
wrapX: true
})
});
}
// 创建天地图图层组
function createTiandituLayers(key) {
return [
createTiandituWMTS('vec', key),
createTiandituWMTS('cva', key)
];
}
9.6 瓦片错误处理
9.6.1 加载失败处理
// 瓦片加载错误处理
const tileSource = new XYZ({
url: 'https://example.com/tiles/{z}/{x}/{y}.png',
tileLoadFunction: function(imageTile, src) {
const image = imageTile.getImage();
image.onerror = function() {
// 加载失败时使用占位图
image.src = '/images/tile-error.png';
};
image.src = src;
}
});
// 监听瓦片加载事件
tileSource.on('tileloaderror', (event) => {
const tileCoord = event.tile.getTileCoord();
console.error(`瓦片加载失败: z=${tileCoord[0]}, x=${tileCoord[1]}, y=${tileCoord[2]}`);
});
// 加载进度跟踪
class TileLoadingProgress {
constructor(source) {
this.loading = 0;
this.loaded = 0;
this.errors = 0;
source.on('tileloadstart', () => {
this.loading++;
this.update();
});
source.on('tileloadend', () => {
this.loaded++;
this.update();
});
source.on('tileloaderror', () => {
this.errors++;
this.loaded++;
this.update();
});
}
update() {
const total = this.loading;
const done = this.loaded;
const progress = total > 0 ? done / total : 1;
console.log(`加载进度: ${Math.round(progress * 100)}% (${done}/${total}, 错误: ${this.errors})`);
if (done >= total) {
this.reset();
}
}
reset() {
this.loading = 0;
this.loaded = 0;
this.errors = 0;
}
}
9.7 本章小结
本章详细介绍了栅格数据与瓦片服务:
- 瓦片原理:金字塔、编号方案、分辨率
- 瓦片网格:创建、WMTS 网格、自定义
- 瓦片加载:自定义加载、缓存、预加载
- WMS 服务:瓦片、图像、查询
- WMTS 服务:配置、天地图
- 错误处理:失败处理、进度跟踪
关键要点
- 理解瓦片金字塔原理
- 掌握瓦片网格配置
- 实现瓦片缓存和预加载
- 正确处理瓦片加载错误

浙公网安备 33010602011771号