vue + cesium(1): 3d地图开发

271 阅读2分钟

本文中使用版本号:

"vue": "2.6.10",
"cesium": "^1.68.0",
"webpack": "^4.41.2",
"copy-webpack-plugin": "^5.0.5",
"terser-webpack-plugin": "^4.2.3",

1.安装cesium

npm install cesium@1.68.0 --save

2.在main.js中引用样式和自己封装的cesium方法

import { MyCesium } from "@/util/3dMap/MyCesium"
import 'cesium/Widgets/widgets.css'

Vue.prototype.$MyCesium = MyCesium;

vue.config.js配置

/**
 * 配置该文件可以参考:
 * https://cli.vuejs.org/zh/config/#%E7%9B%AE%E6%A0%87%E6%B5%8F%E8%A7%88%E5%99%A8
 *
 */
const path = require("path");
const webpack = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const cesiumSource = "node_modules/cesium/Source";
const cesiumWorkers = "../Build/Cesium/Workers";
const resolve = (dir) => {
	return path.join(__dirname, dir);
};
//配置路径json文件
const TerserPlugin = require('terser-webpack-plugin');
const cfgJson = require('./serverconfig.json') //这里引入的地址就是后面新建serverconfig.json的地址
var createServerConfig = function (compilation) {
  return JSON.stringify(cfgJson);
}
console.log('path', path.join('./dist', 'rrr'))
let devServeUrl = require('./src/config/devServe')
// 基础路径,发布前修改这里,当前配置打包出来的资源都是相对路径
// let publicPath = './'
let publicPath = process.env.NODE_ENV === "development" ? "/" : "./"
module.exports = {
  publicPath: publicPath,
  lintOnSave: true,
  productionSourceMap: false,
  css: {
    // 忽略 CSS order 顺序警告
    extract: {ignoreOrder: true}
  },
  chainWebpack: config => {
    const entry = config.entry('app')
    entry
      .add('babel-polyfill')
      .end()
    entry
      .add('classlist-polyfill')
      .end()
    config
      .plugin('GenerateAssetPlugin')
      .use('generate-asset-webpack-plugin', [{
        filename: 'serverconfig.json',
        fn: (compilation, cb) => {
          cb(null, createServerConfig(compilation));
        },
        extraFiles: []
      }])
  },
  // 配置转发代理
  devServer: {
    port: 8520,
    proxy: {
      '/api': {
        target: devServeUrl.baseUrl,
        ws: true,
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      },
      '/video': {
        target: devServeUrl.videoUrl,
        ws: true,
        pathRewrite: {
          '^/video': '/video'
        }
      }
    }
  },
  configureWebpack: {
    resolve: {
			alias: {
				'@': resolve('src'),
				// cesium 1
				cesium: path.resolve(__dirname, cesiumSource) // 用source会导致每次build所有cesium js;用DebugDir,则存在widgets.css引用问题
			}
    },
    amd: {
			// cesium 2
			toUrlUndefined: true
		},
		module: {
			// cesium 3 不加这个配置会报require引入警告
      unknownContextCritical: false,
      rules: [{test: /\.(gltf)$/,loader: 'url-loader'}],
		},
		// cesium 4
		plugins: [
			new webpack.DefinePlugin({
				// Define relative base path in cesium for loading assets
				CESIUM_BASE_URL: JSON.stringify("./")
			}), // 对build生效,拷贝到dist目录下。如:dist/Assets
			new CopyWebpackPlugin([{
				from: path.join(cesiumSource, cesiumWorkers),
        // from: path.join(cesiumSource, "Workers"),
				to: "Workers"
			}]),
			new CopyWebpackPlugin([{
				from: path.join(cesiumSource, "Assets"),
				to: "Assets"
			}]),
			new CopyWebpackPlugin([{
				from: path.join(cesiumSource, "Widgets"),
        to: "Widgets",
        // to: path.join('./dist', "Widgets")
			}]),
			new webpack.ProvidePlugin({
				Cesium: ["cesium/Cesium"], // Cesium对象实例可在每个js中使用而无须import
      }),
		],
    optimization:{
        minimizer: [new TerserPlugin({ terserOptions: { compress: { drop_console: true } } })]
        // minimize: process.env.NODE_ENV === "production" ? true : false
    }
}
}

3.封装方法:

使用到的方法封装

3.1:MyCesium.js(地图公共方法及初始化viewer)

vue + cesium: 地图公共方法及初始化viewer(MyCesium.js) - 掘金 (juejin.cn)

3.2:params.js(地图参数)

vue+Cesium: 地图参数(params.js) - 掘金 (juejin.cn)

3.3:transform.js(地图转换方法)

vue + cesium: 地图转换方法(transform.js) - 掘金 (juejin.cn)

3.4:infoWindow.js(地图点击弹框框方法封装)

vue + cesium: 地图点击弹框框方法封装(infoWindow.js) - 掘金 (juejin.cn)

3.5:init.js(加载3d渔港全景)

vue + cesium:加载3d模型(init.js) - 掘金 (juejin.cn)

3.6: initShipBoat.js(加载渔船)

vue + cesium: 加载渔船(initShipBoat.js) - 掘金 (juejin.cn)

3.7: draw.js(绘制点线面 方法封装)

vue + cesium: 绘制点线面 方法封装(draw.js) - 掘金 (juejin.cn)

3.8: 渔船轨迹绘制

vue+cesium: 渔船轨迹绘制(可运动)(TrailLineAnimate.js) - 掘金 (juejin.cn) vue + cesium: 渔船轨迹(无动画)(trackPlayback.js) - 掘金 (juejin.cn)

4.使用

image.png 本文中有多个页面使用3d地图 因此封装为组件 传入routerData 和路由进行匹配

<map3d v-show="show3dMap" style="height: 100%" :routerData="routerData" id="cesiumContainerFishery"></map3d>

routerData: {
    moduleName: '模块名称',
    modulePath: '模块地址',
}

组件

<template>
    <div>
        <div :ref="id" :id="id" style="height: 100%"></div>
        <!-- 绘制弹窗 -->
        <edit-draw-dialog  moduleName="BerthManage" v-if="showTool" :show.sync="showTool" :currentInfo="currentInfo" @handleClose="handleClose" :isEditDraw="isEditDraw" :editDrawData="editDrawData"></edit-draw-dialog>
        <!-- 需要跟随鼠标移动的 加上style="position: absolute" 不需要则不加 -->
        <!-- 泊位信息弹窗 -->
        <div id="portInfoWindow" style="position: absolute">
        <port-info v-show="showPortInfo" :show.sync="showPortInfo" :portId="portDetailInfo.id" @portClose="handlePortClose"></port-info>
        </div>
        <!-- 渔船信息弹窗 -->
        <div id="3dDrawPopup" style="position: absolute">
            <shipPopup v-show="showShipHoverPopup" :is3dMap="true" :show-ship-hover-info="showShipHoverInfo" :show-ship-hover-popup="showShipHoverPopup"></shipPopup>
        </div>
        <div id="3dShipInfo" >
            <ship-info ref="shipInfo" v-if="showShipInfo" :show.sync="showShipInfo" :ship-id="shipInfo.shipId" :shipProps="shipProps" :showFooter="false" @beforeClose="closeShipInfo" :is3dMap="true"></ship-info>
        </div>
        <!--地图摄像头点击 弹框-->
        <div id="videoMonitorWindow" style="position: absolute">
            <camera-popup
            ref="cameraPopup3d"
            id="cameraPopup3d"
            @removePopup="removePopup"
            v-show="showCamera"
            videoId="video3dIdOut"
            ></camera-popup>
        </div>
        <!-- 水质检测 -->
        <div id="waterModalWindow" style="position: absolute">
            <WaterModal v-if="showWater" :show.sync="showWater" :waterId="showWaterInfo.id" :is3dMap="true" @waterClose="waterClose"></WaterModal>
        </div>
        <!-- 视频 -->
        <MapVideoModal
        :show.sync="showVideoCom"
        v-if="showVideoCom"
        :videoData="videoData"
        ref="MapVideoModal"
        ></MapVideoModal>
    </div>
</template>
<script>
import {initLoadModel} from '@/util/3dMap/init'
import EditDrawDialog from "@/views/mapPlatform/components/editDrawModal/index";
import {$Params} from "@/util/3dMap/params";
import {initLoadShipBoatModel} from "@/util/3dMap/initShipBoat";
import {getBerthShipPositions} from '@/api/fisheryAreaSupervision'
import {subjectSelectAllPort} from "@/api/map/drawSubject";
import {getShipBerthIotLayout} from "@/api/fisheryAreaSupervision";
import {transFormDataFromResponse} from "@/util/3dMap/transform";
import PortInfo from "@/views/mapPlatform/components/portInfo/portInfo3d";
import shipPopup from "@/views/mapPlatform/components/shipPopup/index";
import ShipInfo from "@/views/mapPlatform/components/shipInfo/shipInfo";
import CameraPopup from "@/views/mapPlatform/components/cameraPopup/cameraPopup";
import WaterModal from "@/views/mapPlatform/components/waterModal";
import MapVideoModal from "@/views/mapPlatform/components/mapVideoModal";
import { getEquipSingle } from "@/api/baseInfo";
export default {
    components: {EditDrawDialog, PortInfo, shipPopup, ShipInfo, CameraPopup, WaterModal, MapVideoModal},
    props: {
        id: {
            type: String,
            default: 'cesiumContainerFishery'
        },
        routerData: {
            type: Object,
            default: () => {}
        }
    },
    data () {
        return {
            showTool: false,
            //当前标绘物编辑弹框信息
            currentInfo: {},
            showPortInfo: false,
            portDetailInfo: {},
            isEditDraw: false,
            editDrawData: {},
            showShipInfo: false,
            showHoverPopup: false,
            hoverInfo: {},
            shipProps: {},
            shipInfo: {},
            showShipHoverInfo: {},
            showShipHoverPopup: false,
            showCamera: false,
            showWater: false,
            showWaterInfo: {},
            showVideoCom: false, //视频弹窗组件
            videoData: {}, //首页单个视频数据
        }
    },
    mounted () {
        // 获取渔船点位
        this.drawLayer()
        // 初始化3d地图
        this.$refs[this.id].oncontextmenu = function () { return false }
        this.$MyCesium.init(this.id)
        this.$MyCesium.viewer.entities.collectionChanged.addEventListener(this.onChanged);
        //加载3d模型和动态水域
        initLoadModel({viewer: this.$MyCesium.viewer}) 
        // 泊位绘制
        this.handlePort()
        // 设备 摄像头、水质监测点
        this.handleDevice()
        // 通过$Bus监听地图点击事件
        // 水质检测
        this.$Bus.$off('3dshowWaterPopup') && this.$Bus.$on('3dshowWaterPopup', (data, info) => {
            this.showWater = data
            this.showWaterInfo = info
        })
        // 渔船双击
        this.$Bus.$off('3dshipClick').$on('3dshipClick', (data, props) => {
            this.shipProps = props
            if (data) {
                console.log('shipClick', data)
                this.$store.commit('SET_isShowIcon', false)
                this.showShipInfo = true
                this.shipInfo = props
                this.$nextTick(() => {
                    this.$refs['shipInfo'].showShipTrack = false
                })
            } else {
                this.showShipInfo = false
                this.$store.commit('SET_isShowIcon', true)
                
            }
        })
        //渔船鼠标移入事件
        this.$Bus.$off('3dshowShipPopup') && this.$Bus.$on('3dshowShipPopup', (data, info) => {
            this.showShipHoverPopup = data
            this.showShipHoverInfo = info
        })
        this.$Bus.$off('3dhandleDraw3d') && this.$Bus.$on('3dhandleDraw3d', (methods, type) => {
            if(methods == 'draw') {
                this.$store.commit('SET_isShowIcon', false)
                this.showTool = !this.showTool
                this.currentInfo = {
                    type:$Params.drawType[type],
                    methods: methods,
                    drawType: type
                }
            }
        })
        this.$Bus.$off('3deditPortInfo') && this.$Bus.$on('3deditPortInfo', (value)=>{
            console.log(value, 'pppppppppp')
            this.$store.commit('SET_isShowIcon', !value.isEdit)
            this.showTool = value.isEdit
            this.isEditDraw = value.isEdit
            this.editDrawData = value.data
        })
         // // 泊位点击事件
        this.$Bus.$off("3dhandlePortDialog") && this.$Bus.$on("3dhandlePortDialog", (data) => {
            console.log(data, '泊位点击事件')
            if (!data) return this.showPortInfo = false
            else {
                this.showPortInfo = true;
                this.portDetailInfo = data;
            }
            console.log(this.showPortInfo, 'showPortInfo')
        });
        this.$Bus.$off("3ddeviceClick").$on("3ddeviceClick", (show, data) => {
            console.log('设备点击3d', show, data)
            // this.camera.show = true
            this.getDeviceVideo(data);
            this.showCamera = show;
        })
        //触发视频监控弹窗按钮
        this.$Bus.$off("show3dVideoDialog").$on("show3dVideoDialog", (isShow, data) => {
            this.showVideoCom = isShow;
            this.videoData = data;
            this.showHideLabelCommon(false);
        })
    },
    methods: {
        showHideLabelCommon (value) {
            this.$store.commit('setLeftShowIcon', value)
            this.$store.commit('setRightShowIcon', value)
        },
        waterClose () {
            this.showWater = false
            this.$MyCesium.isShowSelectionIndicator(false)
        },
        //  单个设备视频流
        getDeviceVideo(data) {
            console.log(data, 'ppppppppppppppp')
            let id = data.id;
            getEquipSingle(id).then((res) => {
                if (res.data.code == 0) {
                this.$nextTick(() => {
                    this.$refs.cameraPopup3d.deviceName = res.data.data.iotDeviceName;
                    this.$refs.cameraPopup3d.urlLinuxData = res.data.data;
                });
                }
            });
        },
        removePopup(data) {
            this.showCamera = data;
            this.$MyCesium.isShowSelectionIndicator(false)
        },
        closeShipInfo(){
            this.$store.commit('SET_isShowIcon', true)
            this.$MyCesium.isShowSelectionIndicator(false)
        },
        handlePortClose () {
            this.showPortInfo = false
            this.$MyCesium.isShowSelectionIndicator(false)
        },
        showHideLabelCommon (isShowIcon) {
            this.$store.commit("SET_isShowIcon", isShowIcon);
        },
        onChanged(collection, added, removed, changed){
            var msg = 'Added ids';
            for(var i = 0; i < added.length; i++) {
                msg += '\n' + added[i].id;
            }
        },
        
        handleClose() {
            this.showTool = false
            this.$store.commit('SET_isShowIcon', true)
        },
        // 绘制点位
        async drawLayer(timestamp = 0) {
            const res = await this.getCrewPosition(timestamp)
            console.log('drawLayer', res)
            if (!res || !res.positions || res.positions.length === 0) return this.$message.error('渔船点位数据不存在')
            // 递归函数
            const digui = () => {
                let timer = null
                const run = () => {
                    timer = setTimeout(() => {
                        if (this.$MyCesium.viewer) {
                            initLoadShipBoatModel(timestamp, res.positions)
                            this.getCrewPositionDelay(res.timestamp)
                        } else digui()
                    }, 400)
                }
                const stop = () => {
                    clearTimeout(timer)
                    timer = null
                }

                return {
                    run: run,
                    stop: stop
                }
            }
            let moduleName = this.$route.name // 记录当前模块名称
            let bool = this.isHomeToCur(this.routerData.modulePath)
            if (bool) {
                moduleName = this.routerData.moduleName
            }
            const executor = digui() // 获取执行器

            if (!res) return executor.stop() // 接口报错时递归清除
            if (moduleName === this.routerData.moduleName) { // 避免板块切换时接口轮询
                executor.run()
            }
        },
        // 获取船员位置数据
        getCrewPosition(timestamp) {
            return new Promise((resolve) => {
                getBerthShipPositions({timestamp: timestamp}).then(res => {
                    let data = res.data
                    console.log('getCrewPosition', data)
                    if (data.success) {
                        if (data.data && data.data.positions) {
                            resolve(data.data)
                        } else {
                            resolve(false)
                        }
                    }
                }).catch(err => console.log(err))
            })
        },
        // 每隔一段时间获取一次增量的船员位置数据
        getCrewPositionDelay: (() => {
            let timer = null
            return function (maxTimestamp) {
                clearTimeout(timer)
                let moduleName = this.$route.name // 记录当前模块名称
                let bool = this.isHomeToCur(this.routerData.modulePath)
                if (bool) {
                    moduleName = this.routerData.moduleName
                }
                if (moduleName === this.routerData.moduleName) { // 避免板块切换时接口轮询
                    timer = setTimeout(() => {
                        this.drawLayer(maxTimestamp)
                    }, 60000)
                }
            }
        })(),
        handlePort () {
            subjectSelectAllPort().then(res=>{
                if (res.data.code == 0) {
                    this.$MyCesium.removeDataSource('DrawPort')
                    this.$MyCesium.addEntitysCollection('DrawPort', transFormDataFromResponse(res.data.data))
                }
            })
        },
        handleDevice () {
            getShipBerthIotLayout().then(res=>{
                if (res.data.code == 0) {
                    // 绘制摄像头
                    this.$MyCesium.removeDataSource('VideoMonitor')
                    this.$MyCesium.addDeviceEntitysCollection('VideoMonitor',transFormDataFromResponse(res.data.data.camera))
                    // 绘制水质监测点
                    this.$MyCesium.removeDataSource('waterQuality')
                    this.$MyCesium.addDeviceEntitysCollection('waterQuality',transFormDataFromResponse(res.data.data.waterQuality))
                }
            })
        }
    },
    beforeDestroy () {
        this.$MyCesium.removeDataSources()
        this.$MyCesium.viewer = undefined
        this.$MyCesium.handler = undefined
        this.$MyCesium.curentEntity = null
        this.$MyCesium.model = null
    }
}
</script>

5. 绘制点、面、线

import {$Draw} from "@/util/3dMap/draw";
var DRAW_TYPE = {
    Point: 'Point', // 点
    PolyLine: 'PolyLine', // 线
    Polygon: 'Polygon', // 面
    Marker: 'Marker', // 标记
};
draw(type) {
    $Draw().draw({drawingMode: type})
},

6.渔船轨迹

import TrailLineAnimate from "@/util/3dMap/TrailLineAnimate";
data(){
    return{
            //轨迹实例
          TrailLineAnimateInstance: null,
    }
},
// 使用
// 初始化
this.TrailLineAnimateInstance = new TrailLineAnimate(this.$MyCesium.viewer)
// 获取到数据后进行播放
this.TrailLineAnimateInstance.play(this.shipTrackList, this.speed)
// 清除实例
this.TrailLineAnimateInstance.clear()

7.遇到的问题

7.1 发布线上环境报错

在本地开发时3d地图加载是正常的 发布到线上环境时报错 原因:因为在vue.config.js复制的文件被加载到底层 未能找到 解决方法:在node_modules/cesium下面复制Assets、ThirdParty、Widgets、Workers文件夹到public/static文件夹下面 并在index.html文件中引入

image.png

7.2 使用new Cesium.Cesium3DTileset时模型未加载

解决办法: 把ThirdParty复制到public文件夹下 image.png