第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 本章小结

本章详细介绍了栅格数据与瓦片服务:

  1. 瓦片原理:金字塔、编号方案、分辨率
  2. 瓦片网格:创建、WMTS 网格、自定义
  3. 瓦片加载:自定义加载、缓存、预加载
  4. WMS 服务:瓦片、图像、查询
  5. WMTS 服务:配置、天地图
  6. 错误处理:失败处理、进度跟踪

关键要点

  • 理解瓦片金字塔原理
  • 掌握瓦片网格配置
  • 实现瓦片缓存和预加载
  • 正确处理瓦片加载错误

← 上一章:矢量数据与样式 | 返回目录 | 下一章:Control控件系统 →

posted @ 2026-01-08 11:37  我才是银古  阅读(8)  评论(0)    收藏  举报