Skip to Content

镜像地球开放平台

WebGL 方式接入

通过 WebGL 方式展示气象图层,支持动态调色、高性能渲染及前端直接点击取值,适用于大屏展示和复杂交互场景。

WebGL 渲染方式能够带来更高的渲染效率、更平滑的视觉效果,并支持在前端直接进行点击取值(而不需要为了取值额外发起网络请求)。本指南将展示如何使用 leaflet-wind 在项目中加载图层。

本教程使用 leaflet-wind 插件,它内置了硬件加速渲染支持。

1. 业务流程

与普通的图片图层不同,WebGL 方式返回的是编码后的数据栅格(通常为 jpeg 格式的带 Exif 数据的单张图像)。前端获取图像后,利用 Shader 对数据进行解码,并根据配置的色板进行染色渲染。

  1. 获取时间:调用 meta 接口获取所需的最新数据时间。
  2. 构建编码图 URL:将 timemodelelement_level 组合。注意 WebGL 使用的接口路径最后通常带有 .jpeg?size=2048,例如 https://api.mirror-earth.com/api/vis/{model}/{element}/{time}.jpeg?size=2048&apikey={apikey}
  3. 初始化图层数据源:利用 leaflet-windImageSource 并指定 decodeType: DecodeType.imageWithExif 载入图像。
  4. 加载 WebGL 图层:创建 WebglLayer,通过 styleSpec 配置色板与透明度,添加到地图上。
  5. 前端直接取值:使用图层自带的 picker 方法直接读取点击处的色值/数据值。

2. 示例应用 (Playground)

下面示例展示了如何在 Vue 和 React 项目中,异步加载 leafletleaflet-wind 渲染温度图层,并使用本地提供的取值方法。

<template>
  <div class="playground-content">
    <div class="map" ref="mapRef"></div>
    <div v-if="clickedValue" class="result-box">
      点击结果: {{ clickedValue }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';

const mapRef = ref<HTMLDivElement>();
const clickedValue = ref<string | null>(null);

const API_KEY = 'YOUR_API_KEY';

let map: any;
let heatmapLayer: any;

const fetchTime = async (model: string) => {
  const metaUrl = `https://api.mirror-earth.com/api/vis/${model}/meta?apikey=${API_KEY}&timezone=Asia/Shanghai`;
  const response = await fetch(metaUrl);
  const result = await response.json();
  return result.data[0];
};

async function initMap() {
  if (!mapRef.value) return;

  // 异步加载相关依赖,避免在 ssg 模式下构建报错
  const L: any = await import('leaflet');
  const { WebglLayer, ImageSource, DecodeType, RenderFrom, RenderType } = await import('leaflet-wind');

  map = new L.Map(mapRef.value, {
    zoom: 3,
    center: [35.0, 105.0]
  });

  const tileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', {
    subdomains: ['a', 'b', 'c', 'd'],
  });
  map.addLayer(tileLayer);

  // 1. 获取模型最新时间
  const time = await fetchTime('ecmwf');

  // 2. 构建 WebGL 瓦片 URL
  const dataUrl = `https://api.mirror-earth.com/api/vis/ecmwf/temperature_2m/${time}.jpeg?size=2048&apikey=${API_KEY}&timezone=Asia/Shanghai`;

  const source = new ImageSource('temperature', {
    url: dataUrl,
    coordinates: [
      [-180, 85.051129],
      [180, 85.051129],
      [180, -85.051129],
      [-180, -85.051129],
    ],
    decodeType: DecodeType.imageWithExif,
    wrapX: true,
  });

  // 自定义色板配置(以温度为例)
  const tempColorConfig = [
    [-30, [98, 113, 183, 255]],
    [-20, [57, 97, 159, 255]],
    [-10, [74, 148, 169, 255]],
    [0, [77, 141, 123, 255]],
    [10, [83, 165, 83, 255]],
    [20, [167, 157, 81, 255]],
    [30, [159, 127, 58, 255]],
    [40, [175, 80, 136, 255]]
  ];

  const interpolateColor = tempColorConfig.reduce(
    (result, item) => result.concat(item[0], `rgba(${item[1].join(',')})`),
    [] as any[]
  );

  heatmapLayer = new WebglLayer('temperature', source, {
    styleSpec: {
      'fill-color': ['interpolate', ['linear'], ['get', 'value'], ...interpolateColor],
      opacity: 0.8,
    },
    renderFrom: RenderFrom.rg,
    displayRange: [-50, 50],
    renderType: RenderType.colorize,
  });

  map.addLayer(heatmapLayer);

  // 3. 前端直接点击取值
  map.on('click', async (e: any) => {
    if (!heatmapLayer) return;
    
    // 使用图层自带的 picker 取值
    // 返回格式为数组,单层图层数值通常对应数组的第一项 v1
    const [v1, v2, v3, v4] = await heatmapLayer.picker(e.latlng);
    
    if (v1 !== undefined && v1 !== null) {
      clickedValue.value = `坐标: ${e.latlng.lat.toFixed(2)}, ${e.latlng.lng.toFixed(2)} | 数值: ${v1.toFixed(2)}`;
    } else {
      clickedValue.value = `无数据 (可能超出图层范围)`;
    }
  });
}

onMounted(() => {
  initMap();
});

onUnmounted(() => {
  if (map) {
    map.remove();
  }
});
</script>

<style scoped>
@import 'https://esm.sh/leaflet/dist/leaflet.css';

.playground-content {
  width: 100%;
  height: 500px;
  position: relative;
}

.map {
  width: 100%;
  height: 100%;
}

.result-box {
  position: absolute;
  bottom: 20px;
  left: 20px;
  z-index: 1000;
  padding: 10px;
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  color: #333;
}
</style>

Previous

图片方式接入

Next

风场粒子动画接入