cesium 实现克里金生成矢量等值面,使用worker浏览器线程

需求背景

需要实现一个上百个栅格存在数值的等值面,为了提高用户体验
1.需要插值,考虑使用 kriging 算法,原有的kriging算法,未优化前,计算300点位就会造成栈溢出
2.kriging前端计算量大,使用worker浏览器线程,避免阻塞主线程

解决效果

index.vue

<!--/**
* @author: liuk
* @date: 2024-07-29
* @describe:克里金生成矢量等值面
*/-->
<template>
  <div class="ybPanel-wrap">
    <div class="operation-area">
      <div class="ctrl-row">
        <el-row style="width: 100%;height: 100%">
          <el-col :span="24">
            <el-form-item label="" style="margin-top: 5px">
              <el-checkbox v-model="gridShow" style="position: relative;top:-5px;margin-right: 20px"
                           @change="gridShowChange">
                <span style="font-size: 14px;color: #fff">--</span>
              </el-checkbox>
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="">
              <el-radio-group v-model="formData.type">
                <el-radio-button value="h">--</el-radio-button>
                <el-radio-button value="total">--</el-radio-button>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
      </div>
      <div class="time-player">
        <span class="iconfont icon-icon_prev" @click="doPrev"></span>
        <span class="play-icon iconfont" @click="togglePlay" :class="playFlag ? 'icon-pause' : 'icon-play'"/>
        <span class="iconfont icon-icon_next2" @click="doNext"></span>
        <el-slider v-model="curTime" :show-tooltip="false" :min="timeList[0]" :max="timeList[timeList.length - 1]"
                   tooltip-class="toolTip"
                   :step="60*60*1e3" style="width: 700px;margin-left: 10px"/>
        <div class="tip-area">
          <div class="tip" ref="tipRef">
            <span>{{ moment(curTime).format(`YYYY-MM-DD HH:mm:ss`) }}</span>
          </div>
          <div class="min-val">{{ moment(timeList[0]).format(`YYYY-MM-DD HH:mm:ss`) }}</div>
          <div class="max-val">{{ moment(timeList[timeList.length - 1]).format(`YYYY-MM-DD HH:mm:ss`) }}</div>
        </div>
        <el-select class="tip-area-select" v-model="speedType" size="small" placeholder="" style="width: 40px">
          <el-option v-for="item in speedOptions" :key="item.value" :label="item.label" :value="item.value"/>
        </el-select>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import {reactive, toRefs, onMounted, defineProps, defineEmits, watch, onUnmounted, ref, computed} from "vue"
import {theHourTime, formatToFixed, cartesianToWgs84} from "@/utils/dictionary";
import moment from "moment";
import {ElMessage, ElLoading} from "element-plus";
import {getDrainageBasinGridByCY, getLon2LatByCY, getTimeByCY} from "@/api/forecast";
// Props
const props = defineProps(['modelValue'])
// Emits
const emit = defineEmits(['update:modelValue'])
// Refs
const tipRef = ref(null)


const model = reactive({
  formData: {
    type: 'h',
    maxEndTime: moment().valueOf(),
  },
  gridShow: false,// 是否展示栅格
  playFlag: false,// 是否播放
  curTime: 0, // 当前时间
  speedType: 1,// 播放数据
  timeList: [],// 所有数据时间
  curLoadingIndex: 0,
})
const {
  formData,
  gridShow,
  playFlag,
  curTime,
  timeList,
  speedType,
  curLoadingIndex
} = toRefs(model)

watch(() => model.curTime, (curTime) => {
  if (!tipRef.value) return
  tipRef.value.style.left = (curTime - model.timeList[0]) / (model.timeList[model.timeList.length - 1] - model.timeList[0]) * 100 + "%"
  krigingModel.loading || updateGridColor()
})

onMounted(() => {
  getlist()
})

onUnmounted(() => {
  window.clearInterval(timer)
  viewer.entities.remove(heatmapEntity)
  viewer.dataSources.remove(pointGridDatasouce);
})

const getlist = async () => {
  // const {data: data1} = await getLon2LatByCY()
  // krigingModel.lonLats = data1
  createHeatMap()
  // const {data: data2} = await getTimeByCY()
  // model.timeList = data2.map(item => item * 1e3)
  // const longitudes = krigingModel.lonLats.map(item => item.split("_").map(Number)[0])
  // const latitude = krigingModel.lonLats.map(item => item.split("_").map(Number)[1])
  const timeList = [1724641200000, 1724644800000, 1724648400000, 1724652000000, 1724655600000, 1724659200000, 1724662800000, 1724666400000, 1724670000000, 1724673600000, 1724677200000, 1724680800000, 1724684400000, 1724688000000, 1724691600000, 1724695200000, 1724698800000, 1724702400000, 1724706000000, 1724709600000, 1724713200000, 1724716800000, 1724720400000, 1724724000000]
  const loadingInstance = ElLoading.service({
    fullscreen: true,
    text: '模型生成中...',
    maskColor: 'rgba(41, 12, 12, .5)'
  })
  model.timeList = timeList
  krigingModel.dataMap.clear()
  const worker = new Worker('/worker/worker.js');
  worker.postMessage({timeList});

  const colors = [
    {min: 0, max: 1, color: "rgba(233,233,233,.03)"},
    {min: 1, max: 1.5, color: "rgba(163,243,146,.03)"},
    {min: 1.6, max: 6.9, color: "rgba(57,165,1,.03)"},
    {min: 7, max: 14.9, color: "rgba(99,183,255,.03)"},
    {min: 15, max: 39.9, color: "rgba(244,171,24,.03)"},
    {min: 40, max: 49.9, color: "rgba(234,27,231,.03)"},
    {min: 50, max: 100, color: "rgba(157,0,79,.03)"},
  ]
  worker.onmessage = function (e) {
    const {time, grid, data, index} = e.data;
    const canvasDom = document.createElement('canvas');
    canvasDom.className = `heatMap-box-${time}`
    kriging.plot(canvasDom, grid, [112.82999504183772, 113.19000495776265], [25.50999504223745, 25.950004957462852], colors)
    krigingModel.dataMap.set(time, {grid, data, canvasDom, time: new Date(time).toLocaleString()})
    model.curLoadingIndex = index
    if (krigingModel.loading) {
      model.curTime = model.timeList[0]
      updateGridColor()
      krigingModel.loading = false
      loadingInstance.close()
    }
  };
  worker.onerror = function (error) {
    console.error('Worker error:', error);
  };
}

let timer
const togglePlay = () => {
  model.playFlag = !model.playFlag
  if (model.playFlag) {
    if (model.curTime >= model.timeList[model.timeList.length - 1]) model.curTime = model.timeList[0]
    timer = window.setInterval(() => {
      if (model.curTime >= model.timeList[model.timeList.length - 1]) {
        window.clearInterval(timer)
        return
      }
      model.curTime += 60 * 60 * 1e3 / 60
    }, 1000 / model.speedType / 60)
  } else {
    window.clearInterval(timer)
  }
}

const doPrev = () => {
  model.curTime -= 60 * 60 * 1e3
}

const doNext = () => {
  model.curTime += 60 * 60 * 1e3
}

const gridShowChange = (bool) => {
  pointGridDatasouce.show = bool
}

const speedOptions = [
  {label: "0.5x", value: 0.5},
  {label: "1x", value: 1},
  {label: "2x", value: 2},
  {label: "3x", value: 3},
]
// 地图模块
import {usemapStore} from "@/store/modules/cesiumMap";
import boundaryDataOld from "@/assets/data/boundaryOld20240807.json";
import pointGrid from "@/assets/data/pointGrid20240815.json"
import * as turf from '@turf/turf'
import {kriging} from "@/utils/kriging";

const krigingModel = reactive({
  lonLats: [],// 所有点位坐标
  dataMap: new Map(),// 所有点位坐标相对于的降雨量数值
  loading: true
})
const {dataMap} = toRefs(krigingModel)

const mapStore = usemapStore()
const viewer = mapStore.getCesiumViewer();

let heatmapEntity, ctx, heatDoc, pointGridDatasouce;
const polyTurf = turf.polygon([boundaryDataOld.geometry.coordinates[0][0].map(item => ([item[0], item[1]]))]);

const createHeatMap = async () => {
  heatDoc = document.createElement("canvas");
  heatDoc.className = "heatMap-box"
  ctx = heatDoc.getContext("2d");
  heatDoc.width = 180;
  heatDoc.height = 220;
  heatmapEntity = viewer.entities.add({
    rectangle: {
      coordinates: Cesium.Rectangle.fromDegrees(112.82999504183772, 25.50999504223745, 113.19000495776265, 25.950004957462852),
      material: heatDoc
    },
  });
  pointGridDatasouce = await Cesium.GeoJsonDataSource.load(pointGrid, {
    fill: Cesium.Color.fromCssColorString("transparent"),
    clampToGround: true,
  });
  viewer.dataSources.add(pointGridDatasouce);
  pointGridDatasouce.show = false
}

const updateGridColor = async () => {
  if (moment(model.curTime).minutes() !== 0) return
  const temp = krigingModel.dataMap.get(model.curTime)
  if (!temp) {
    ElMessage.warning(`${moment(model.curTime).format("MM月DD日 HH时")}计算中,请稍候`)
    timer && window.clearInterval(timer)
    model.playFlag = false
    model.curTime -= 60 * 60 * 1e3
    return
  }
  // const params = {
  //   forecastTime: moment(model.curTime).unix(),
  //   forecastStartTime: model.formData.type === 'total' ? moment(model.formData.startTime).unix() : "",
  // }
  pointGridDatasouce.entities.values.forEach((entity, i) => {
    const entityPolygon = turf.polygon([entity.polygon.hierarchy.getValue().positions.map(pos => cartesianToWgs84(pos).slice(0, 2))])
    if (!turf.intersect(entityPolygon, polyTurf)) return
    entity.position = Cesium.Cartesian3.fromDegrees(+entity.properties.longitude.getValue(), +entity.properties.latitude.getValue())
    entity.label = new Cesium.LabelGraphics({
      text: formatToFixed(temp.data?.[i] || 0, 1), // 可以根据需要调整标签的文本
      // text: formatToFixed(i, 1),
      font: '14px PingFangSC-Regular, PingFang SC',
      pixelOffset: new Cesium.Cartesian2(0, 5),
      outlineColor: new Cesium.Color.fromCssColorString('black'),
      horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
      fillColor: Cesium.Color.fromCssColorString("#fff").withAlpha(1),
      disableDepthTestDistance: Number.POSITIVE_INFINITY,
      heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0.0, 1e5)
    });
    entity.polyline = new Cesium.PolylineGraphics({
      positions: entity.polygon.hierarchy.getValue().positions,
      material: Cesium.Color.fromCssColorString("#fff").withAlpha(1),
      width: 1,
      clampToGround: true
    })
  });
  heatmapEntity.rectangle.material = temp.canvasDom
}
</script>

woker.js

/**
 * @author: liuk
 * @date: 2024-08-26
 * @describe: 克里金插值
 */
const longitudes = [...]
const latitudes = [...]

importScripts('/worker/kriging.ts')
self.onmessage = async (e) => {
    const {timeList} = e.data;
    const res = await fetch("/worker/---.json")
    const boundaryDataOld = await res.json();
    const ex = boundaryDataOld.geometry.coordinates[0]
    timeList.forEach(time => {
        const data = new Array(396).fill().map(() => Math.random() * 50);
        const variogram = kriging.train(data, longitudes, latitudes, 'exponential', 0, 100);
        const grid = kriging.grid(ex, variogram, 0.0005)
        self.postMessage({time, grid, data});
    });
};


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳晓黑胡椒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值