|
@@ -0,0 +1,390 @@
|
|
|
+<template>
|
|
|
+ <div class="main">
|
|
|
+ <EasyMapComponent
|
|
|
+ class="map"
|
|
|
+ layout="info"
|
|
|
+ @easyMapLoad="mapLoad"
|
|
|
+ />
|
|
|
+ <div class="filter">
|
|
|
+ <CusForm labelWidth="70px" ref="ref_form">
|
|
|
+ <CusFormColumn
|
|
|
+ :span="24"
|
|
|
+ required
|
|
|
+ label="时间"
|
|
|
+ link="datetime"
|
|
|
+ type="datetimerange"
|
|
|
+ v-model:param="queryForm.timeArea"/>
|
|
|
+ <CusFormColumn
|
|
|
+ :span="8"
|
|
|
+ required
|
|
|
+ label="数量"
|
|
|
+ link="number"
|
|
|
+ v-model:param="queryForm.total"/>
|
|
|
+ <CusFormColumn
|
|
|
+ :span="8"
|
|
|
+ required
|
|
|
+ label="页面倍数"
|
|
|
+ link="number"
|
|
|
+ v-model:param="queryForm.multiple"/>
|
|
|
+ <CusFormColumn
|
|
|
+ :span="8"
|
|
|
+ required
|
|
|
+ label="间隔"
|
|
|
+ link="number"
|
|
|
+ v-model:param="queryForm.split"/>
|
|
|
+ <div>船舶数量:{{webglPointLayer?.getSource().getFeatures().length}}</div>
|
|
|
+ <el-button type="primary" @click="onSearch" v-if="shipDataMap.size === 0">查询</el-button>
|
|
|
+ <el-button type="primary" @click="mapFunc.toLocation({position: [109.6915958479584, 19.111636735969318], zoom: 9})">定位</el-button>
|
|
|
+ <template v-if="webglPointLayer">
|
|
|
+ <el-button type="warning" @click="webglPointLayer.setVisible(false)" v-if="webglPointLayer.getVisible()">隐藏船</el-button>
|
|
|
+ <el-button type="success" @click="webglPointLayer.setVisible(true)" v-else>显示船</el-button>
|
|
|
+ </template>
|
|
|
+ </CusForm>
|
|
|
+ </div>
|
|
|
+ <PlayBarCom
|
|
|
+ class="play-bar"
|
|
|
+ ref="ref_playBar"
|
|
|
+ :timeArea="playBar.timeArea"
|
|
|
+ :cachePro="playBar.cachePro"
|
|
|
+ :total="playBar.total"
|
|
|
+ @refresh="onPlayRefresh"
|
|
|
+ />
|
|
|
+ <div ref="ref_mapHover" class="hover-info">
|
|
|
+ <div class="hover-main">
|
|
|
+ <div class="hover-item">
|
|
|
+ <div class="hover-item-label">名称:</div>
|
|
|
+ <div class="hover-item-value">{{mapHover.info?.name}}</div>
|
|
|
+ </div>
|
|
|
+<!-- <div class="hover-item">-->
|
|
|
+<!-- <div class="hover-item-label">类型:</div>-->
|
|
|
+<!-- <div class="hover-item-value">{{mapHover.info?.type}}</div>-->
|
|
|
+<!-- </div>-->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+import {
|
|
|
+ defineComponent,
|
|
|
+ computed,
|
|
|
+ onMounted,
|
|
|
+ ref,
|
|
|
+ reactive,
|
|
|
+ watch,
|
|
|
+ getCurrentInstance,
|
|
|
+ ComponentInternalInstance,
|
|
|
+ toRefs,
|
|
|
+ nextTick
|
|
|
+} from 'vue'
|
|
|
+import {useStore} from 'vuex'
|
|
|
+import {useRouter, useRoute} from 'vue-router'
|
|
|
+import {ElMessage, ElMessageBox} from "element-plus";
|
|
|
+import PlayBarCom from "./play-bar.vue";
|
|
|
+import * as layer from "ol/layer";
|
|
|
+import WebGLVectorLayerRenderer from "ol/renderer/webgl/VectorLayer";
|
|
|
+import * as format from "ol/format";
|
|
|
+import * as source from "ol/source";
|
|
|
+import * as ol from "ol"
|
|
|
+import ShipImg from './AIS.svg'
|
|
|
+import * as geom from "ol/geom";
|
|
|
+
|
|
|
+class WebGLLineLayer extends layer.Layer {
|
|
|
+ createRenderer() {
|
|
|
+ return new WebGLVectorLayerRenderer(this, {
|
|
|
+ disableHitDetection: false,
|
|
|
+ style: {
|
|
|
+ 'stroke-color': ['*', ['get', 'lineColor'], '#3c8317'],
|
|
|
+ 'stroke-width': 2,
|
|
|
+ // 'stroke-line-dash': ['get', 'lineDasharray'],
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: '',
|
|
|
+ components: {
|
|
|
+ PlayBarCom
|
|
|
+ },
|
|
|
+ props: {},
|
|
|
+ setup(props, {emit}) {
|
|
|
+ const store = useStore();
|
|
|
+ const router = useRouter();
|
|
|
+ const route = useRoute();
|
|
|
+ const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
|
|
|
+ const state = reactive({
|
|
|
+ map: <any>null,
|
|
|
+ mapFunc: <any>null,
|
|
|
+ queryForm: {
|
|
|
+ timeArea: [
|
|
|
+ '2024-03-18 00:00:00',
|
|
|
+ '2024-03-18 00:10:00',
|
|
|
+ ],
|
|
|
+ total: 10,
|
|
|
+ multiple: 100,
|
|
|
+ split: 60
|
|
|
+ },
|
|
|
+ playBar: {
|
|
|
+ timeArea: [],
|
|
|
+ total: 0,
|
|
|
+ cache: 0,
|
|
|
+ cachePro: 0,
|
|
|
+ },
|
|
|
+ shipDataMap: new Map(),
|
|
|
+ webglPointLayer: <any>null,
|
|
|
+ webglLineLayer: <any>null,
|
|
|
+ mapHover: {
|
|
|
+ info: {},
|
|
|
+ overlay: <any>null
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const ref_form = ref()
|
|
|
+ const ref_playBar = ref()
|
|
|
+ const ref_mapHover = ref()
|
|
|
+ const mapLoad = (map, func) => {
|
|
|
+ state.map = map
|
|
|
+ state.mapFunc = func
|
|
|
+ initMap()
|
|
|
+ }
|
|
|
+ const initMap = () => {
|
|
|
+ // 线
|
|
|
+ state.webglLineLayer = new WebGLLineLayer({
|
|
|
+ zIndex: 20,
|
|
|
+ source: new source.Vector(),
|
|
|
+ })
|
|
|
+ state.map.addLayer(state.webglLineLayer)
|
|
|
+ // 点
|
|
|
+ state.webglPointLayer = new layer.WebGLPoints({
|
|
|
+ zIndex: 30,
|
|
|
+ source: new source.Vector(),
|
|
|
+ style: {
|
|
|
+ "icon-src": ShipImg,
|
|
|
+ "icon-color": '#095217',
|
|
|
+ "icon-rotation": ['get', 'cogs']
|
|
|
+ }
|
|
|
+ })
|
|
|
+ state.map.addLayer(state.webglPointLayer)
|
|
|
+ state.mapHover.overlay = new ol.Overlay({
|
|
|
+ id: 'ref_mapHover',
|
|
|
+ element: ref_mapHover.value,
|
|
|
+ autoPan: false,
|
|
|
+ offset: [0, -30],
|
|
|
+ positioning: 'bottom-center',
|
|
|
+ stopEvent: true,
|
|
|
+ autoPanAnimation: {
|
|
|
+ duration: 250
|
|
|
+ }
|
|
|
+ })
|
|
|
+ state.map.addOverlay(state.mapHover.overlay)
|
|
|
+ state.map.on('pointermove', function (evt) {
|
|
|
+ if (evt.dragging) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const pixel = state.map.getEventPixel(evt.originalEvent);
|
|
|
+ let feature: any = state.map.forEachFeatureAtPixel(pixel, function (feature) {
|
|
|
+ return feature;
|
|
|
+ });
|
|
|
+ // const point = state.map.forEachFeatureAtPixel(pixel, function (feature) {
|
|
|
+ // if (feature.get('geomType') === 'Point') {
|
|
|
+ // return feature;
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ // if (point) {
|
|
|
+ // feature = point
|
|
|
+ // }
|
|
|
+ if (feature) {
|
|
|
+ state.mapHover.info = {
|
|
|
+ name: feature.get('time'),
|
|
|
+ // type: feature.get('geomType'),
|
|
|
+ }
|
|
|
+ state.mapHover.overlay.setPosition(evt.coordinate)
|
|
|
+ } else {
|
|
|
+ state.mapHover.overlay.setPosition(undefined)
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const onSearch = () => {
|
|
|
+ ref_form.value.submit().then(() => {
|
|
|
+ state.playBar.timeArea = JSON.parse(JSON.stringify(state.queryForm.timeArea))
|
|
|
+ state.playBar.cache = 0
|
|
|
+ state.playBar.total = JSON.parse(JSON.stringify(state.queryForm.total))
|
|
|
+ initWS()
|
|
|
+ }).catch((e) => {
|
|
|
+ ElMessage({
|
|
|
+ message: e[0].message,
|
|
|
+ grouping: true,
|
|
|
+ type: 'warning',
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ const initWS = () => {
|
|
|
+ const ws = new WebSocket(`ws://${location.host}/${store.state.app.apiProxy.shipPlaybackWSApi}/random-line`)
|
|
|
+ ws.onopen = (e) => {
|
|
|
+ const str = {
|
|
|
+ total: state.playBar.total,
|
|
|
+ timeArea: state.playBar.timeArea,
|
|
|
+ bbox: state.mapFunc.getBBOX(),
|
|
|
+ split: state.queryForm.split
|
|
|
+ }
|
|
|
+ ws.send(JSON.stringify(str))
|
|
|
+ }
|
|
|
+ ws.onmessage = (e) => {
|
|
|
+ try {
|
|
|
+ const json = JSON.parse(e.data)
|
|
|
+ setShipData(json)
|
|
|
+ } catch (e) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const setShipData = (arr) => {
|
|
|
+ arr.forEach(v => {
|
|
|
+ const pfs = JSON.parse(JSON.stringify(v.points.features))
|
|
|
+ for (let i = 0; i < state.queryForm.multiple; i++) {
|
|
|
+ v.points.features = v.points.features.concat(pfs)
|
|
|
+ }
|
|
|
+ const lfs = JSON.parse(JSON.stringify(v.lines.features))
|
|
|
+ for (let i = 0; i < state.queryForm.multiple; i++) {
|
|
|
+ v.lines.features = v.lines.features.concat(lfs)
|
|
|
+ }
|
|
|
+ state.shipDataMap.set(v.timestamp, {
|
|
|
+ points: v.points,
|
|
|
+ lines: v.lines,
|
|
|
+ })
|
|
|
+ })
|
|
|
+ state.playBar.cache++
|
|
|
+ const areaH = Math.ceil((new Date(state.playBar.timeArea[1]).getTime() - new Date(state.playBar.timeArea[0]).getTime() + 1000) / (1000 * state.queryForm.split))
|
|
|
+ state.playBar.cachePro = state.playBar.cache / areaH
|
|
|
+ }
|
|
|
+ const onPlayRefresh = ({time, flag, speed}) => {
|
|
|
+ const sss = new Date()
|
|
|
+ // if (flag === 'start' || flag === 'jump') {
|
|
|
+ //
|
|
|
+ // } else if (flag === 'play') {
|
|
|
+ // }
|
|
|
+ try {
|
|
|
+ state.webglLineLayer.getSource().clear()
|
|
|
+ state.webglLineLayer.getSource().addFeatures(new format.GeoJSON().readFeatures(state.shipDataMap.get(time).lines))
|
|
|
+ state.webglPointLayer.getSource().clear()
|
|
|
+ state.webglPointLayer.getSource().addFeatures(new format.GeoJSON().readFeatures(state.shipDataMap.get(time).points))
|
|
|
+ } catch (e) {
|
|
|
+ console.log(e)
|
|
|
+ console.log(time)
|
|
|
+ }
|
|
|
+ console.log((new Date().getTime() - sss.getTime()) / 1000 + '秒')
|
|
|
+ if (speed > 50) {
|
|
|
+ setTimeout(() => {
|
|
|
+ ref_playBar.value.nextPlay()
|
|
|
+ }, 2000)
|
|
|
+ } else {
|
|
|
+ setTimeout(() => {
|
|
|
+ ref_playBar.value.nextPlay()
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ onMounted(() => {
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ ...toRefs(state),
|
|
|
+ mapLoad,
|
|
|
+ onSearch,
|
|
|
+ ref_form,
|
|
|
+ onPlayRefresh,
|
|
|
+ ref_playBar,
|
|
|
+ ref_mapHover,
|
|
|
+ }
|
|
|
+ },
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.main {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ position: relative;
|
|
|
+ justify-content: center;
|
|
|
+ .play-bar {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 2;
|
|
|
+ bottom: 20px;
|
|
|
+ left: 20px;
|
|
|
+ width: calc(100% - 400px);
|
|
|
+ }
|
|
|
+ .filter {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 2;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 450px;
|
|
|
+ background-color: #FFFFFF;
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.hover-info {
|
|
|
+ $footH: 10px;
|
|
|
+ width: 300px;
|
|
|
+ background: linear-gradient(180deg, #3874C9 0%, #0043C4 100%);
|
|
|
+ border-radius: 0px 4px 4px 4px;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ &:after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ bottom: -$footH;
|
|
|
+ border-top: $footH solid #0043C4;
|
|
|
+ border-left: $footH solid transparent;
|
|
|
+ border-right: $footH solid transparent;
|
|
|
+ }
|
|
|
+ .hover-info-head {
|
|
|
+ padding: 0 4px;
|
|
|
+ height: 18px;
|
|
|
+ position: absolute;
|
|
|
+ top: -18px;
|
|
|
+ left: 0;
|
|
|
+ font-size: 12px;
|
|
|
+ font-family: PingFang SC;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #FFFFFF;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ line-height: 8px;
|
|
|
+ background: linear-gradient(180deg, #3874C9 0%, #0043C4 100%);
|
|
|
+ border-radius: 2px 2px 0 0;/* 设置圆角 */
|
|
|
+ >img {
|
|
|
+ margin: 0 4px 0 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .hover-info-close {
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ top: -16px;
|
|
|
+ }
|
|
|
+ .hover-main {
|
|
|
+ width: 100%;
|
|
|
+ height: auto;
|
|
|
+ padding: 10px;
|
|
|
+ .hover-item {
|
|
|
+ display: flex;
|
|
|
+ .hover-item-label {
|
|
|
+ min-width: 42px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-family: PingFang SC;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #08FFFF;
|
|
|
+ line-height: 20px;
|
|
|
+ }
|
|
|
+ .hover-item-value {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 14px;
|
|
|
+ font-family: PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #FFFFFF;
|
|
|
+ line-height: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|