caner 2 years ago
parent
commit
3a070f09b9
1 changed files with 706 additions and 0 deletions
  1. 706 0
      map-3d.vue

+ 706 - 0
map-3d.vue

@@ -0,0 +1,706 @@
+<script setup lang='ts'>
+import {
+  Viewer, ScreenSpaceEventType, Cartesian3,
+  Math as CMath, Color, EventHelper, UrlTemplateImageryProvider,
+  DistanceDisplayCondition, ScreenSpaceEventHandler, WebMercatorTilingScheme,
+  VerticalOrigin, HeightReference, Cartesian2, Cartographic, GeoJsonDataSource, SceneMode, WebMapTileServiceImageryProvider, GeographicTilingScheme
+} from 'cesium'
+import {
+  computed, onMounted, onUnmounted, ref, watch
+} from 'vue'
+// import useStore from '@/pages/store/index'
+import lnglatIcon from '@/assets/icons/lnglat.svg'
+import camera from '@/assets/icons/camera.svg'
+import broadcast from '@/assets/icons/broadcast.svg'
+import preBranke from '@/assets/icons/preBranke.svg'
+import carBranke from '@/assets/icons/carBranke.svg'
+import { NInput, useNotification, useDialog } from 'naive-ui'
+import EquipmentService, { TreeList, Device } from '@/services/equipmentTree.service'
+
+const notification = useNotification()
+const dialog = useDialog()
+const equipmentService = new EquipmentService()
+// const store = useStore()
+const props = defineProps({
+  type: { default: true, type: Boolean },
+  checkList: { default: [], type: Array<TreeList> },
+  treeList: { default: [], type: Array<TreeList> }
+})
+const isAdmin = computed(() => props.type)
+let viewer = null as Any
+let Helper = new EventHelper() as Any
+let handler = null as Any
+const currenIcon = ref({ icon: '', x: 0, y: 0 })
+const icontypes = [
+  {
+    name: '摄像机', type: 'camera', icon: camera, deviceType: 0
+  },
+  {
+    name: '广播', type: 'broadcast', icon: broadcast, deviceType: 1
+  },
+  {
+    name: '人行闸机', type: 'preBranke', icon: preBranke, deviceType: 2
+  },
+  {
+    name: '车行闸机', type: 'carBranke', icon: carBranke, deviceType: 3
+  }
+]
+const modelShow = ref(false)
+const model = ref({
+  deviceId: '',
+  itemId: -1,
+  longitude: '',
+  latitude: '',
+  describeData: '',
+  selfId: '',
+  deviceType: 0
+})
+const rules = {
+  deviceId: [ { required: true, message: '请输入设备ID' } ],
+  itemId: [ { required: true, message: '请选择项目' } ],
+  longitude: [ { required: true, message: '请输入经度' } ],
+  latitude: [ { required: true, message: '请输入维度' } ],
+  describeData: [ { required: true, message: '请输入安装位置描述' } ]
+}
+const options = ref([] as Array<TreeList>)
+
+const formRef = ref()
+const isAdd = ref(false)
+
+/**
+ * 添加路径
+ * @param lngLat 经纬度数组
+ * @param displayMin 最小显示范围
+ * @param displayMax 最大显示范围
+ */
+function addLine(lngLat: Array<number>, displayMin?: number, displayMax?: number) {
+  if (!viewer) return
+  viewer.entities.add({
+    position: Cartesian3.fromDegrees(lngLat[0], lngLat[1]),
+    polyline: {
+      positions: Cartesian3.fromDegreesArray(lngLat),
+      width: 5,
+      material: Color.fromCssColorString('#55F8F8'),
+      clampToGround: true,
+      distanceDisplayCondition: new DistanceDisplayCondition(displayMin, displayMax)
+    }
+  })
+}
+
+/**
+ * 添加label
+ * @param lng 经度
+ * @param lat 纬度
+ * @param icon 图标
+ * @param text 文本内容
+ * @param displayMin 最小显示范围
+ * @param displayMax  最大显示范围
+ */
+function addLable(lng: number, lat: number, icon: string, text: string, displayMin?: number, displayMax?: number, oldData?: Device) {
+  if (!viewer) return
+  viewer.entities.add({
+    position: Cartesian3.fromDegrees(lng, lat),
+    billboard: {
+      image: icon || lnglatIcon,
+      width: 25,
+      height: 25,
+      verticalOrigin: VerticalOrigin.BOTTOM,
+      disableDepthTestDistance: 21618529,
+      heightReference: HeightReference.CLAMP_TO_GROUND,
+      distanceDisplayCondition: new DistanceDisplayCondition(displayMin, displayMax)
+    },
+    label: {
+      text,
+      font: '12px MicrosoftYaHei',
+      backgroundColor: Color.fromCssColorString('#0F2830'),
+      showBackground: true,
+      fillColor: Color.WHITE,
+      pixelOffset: new Cartesian2(0, -40),
+      disableDepthTestDistance: 21618529,
+      heightReference: HeightReference.CLAMP_TO_GROUND,
+      distanceDisplayCondition: new DistanceDisplayCondition(displayMin, displayMax)
+    },
+    oldData
+  })
+}
+
+/**
+ * 加载geojson
+ * @param path 文件路径
+ * @param callback
+ * @param displayMin
+ * @param displayMax
+ */
+function loadGeoJson(path:string, displayMin?: number, displayMax?: number) {
+  GeoJsonDataSource.load(path, {
+    stroke: Color.fromCssColorString('#55F8F8'),
+    strokeWidth: 3,
+    clampToGround: false,
+    fill: Color.fromCssColorString('rgba(0,0,0,0)')
+  }).then((res) => {
+    const entities = res.entities.values
+    // console.log(55555, entities)
+    // for (let k = 0; k < entities.length; k++) {
+    //   const el = entities[k]
+    //   const polygon = el.polygon as Any
+    //   const polyline = el.polyline as Any
+    //   const billboard = el.billboard as Any
+    //   if (polygon || polyline || el.billboard) {
+    //     if (polygon) polygon.distanceDisplayCondition = new DistanceDisplayCondition(displayMin, displayMax)
+    //     if (polyline) polyline.distanceDisplayCondition = new DistanceDisplayCondition(displayMin, displayMax)
+    //     if (billboard) billboard.distanceDisplayCondition = new DistanceDisplayCondition(displayMin, displayMax)
+    //   }
+    // }
+    if (viewer) {
+      viewer.dataSources.add(res)
+      viewer.scene.requestRender()
+    }
+  })
+}
+
+/**
+ * 移动
+ * @param lng
+ * @param lat
+ * @param profundity 高度
+ */
+function flyTo(lng: number, lat: number, profundity = 19633) {
+  if (!viewer) return
+  viewer.camera.flyTo({
+    destination: Cartesian3.fromDegrees(lng, lat * 0.997, profundity),
+    orientation: {
+      heading: CMath.toRadians(0.0),
+      pitch: CMath.toRadians(-60.0),
+      roll: CMath.toRadians(0.0)
+    }
+  })
+}
+
+/**
+ * 屏幕坐标转经纬度
+ * @param position xy
+ */
+function XYToLngLat(position: { x: number, y: number }) {
+  // 二维屏幕坐标转为三维笛卡尔空间直角坐标(世界坐标)
+  const cartesian3 = viewer.scene.globe.pick(viewer.camera.getPickRay(position), viewer.scene)
+  // 第一步:笛卡尔空间直角坐标系转为地理坐标(弧度制)
+  const cartographic = Cartographic.fromCartesian(cartesian3)
+  // 第二步: 地理坐标(弧度制) 转为经纬度坐标
+  const lat = CMath.toDegrees(cartographic.latitude)
+  const lng = CMath.toDegrees(cartographic.longitude)
+  return { lng, lat }
+}
+
+// 初始化移动添加标点
+function initMove() {
+  // 添加标点
+  const dom = document.querySelector('.maps') as HTMLElement
+  dom.onmousedown = (e: Any) => {
+    const item = e.target.parentElement.dataset.type
+    if (item) {
+      const isNoPro = props.checkList.find((el) => el.lastData === 0 || !el.lastData)
+      if (isNoPro || !props.checkList.length || modelShow.value || props.checkList.length > 1) {
+        let txt = ''
+        if (isNoPro) txt = '请选择项目'
+        if (!props.checkList.length) txt = '请先选择项目'
+        if (modelShow.value) txt = '请完成你的操作再来!'
+        if (props.checkList.length > 1) txt = '请不要勾选多个项目'
+        notification.warning({
+          content: txt,
+          duration: 3000
+        })
+        return
+      }
+      let ismove = false
+      modelShow.value = false
+      dom.onmousemove = (em) => {
+        // 移动样式
+        if (!ismove) ismove = true
+        if (currenIcon.value.icon !== item) currenIcon.value.icon = item
+        currenIcon.value.x = em.x - 15 + window.scrollX
+        currenIcon.value.y = em.y - 100 + window.scrollY
+      }
+      dom.onmouseup = (eu) => {
+        currenIcon.value = { icon: '', x: 0, y: 0 }
+        const { lng, lat } = XYToLngLat({ x: eu.x + window.scrollX, y: eu.y - 74 + window.scrollY })
+        const items = icontypes.find((el) => el.type === item)
+        if (ismove) {
+          addLable(lng, lat, items?.icon!, items?.name!, 1, 16219740)
+          model.value = {
+            deviceId: '',
+            itemId: +props.checkList[0].id,
+            longitude: lng.toString(),
+            latitude: lat.toString(),
+            describeData: '',
+            selfId: '',
+            deviceType: icontypes.filter((el) => el.type === item)[0].deviceType
+          }
+          modelShow.value = true
+          isAdd.value = true
+        }
+        // flyTo(lng, lat)
+        viewer.scene.requestRender()
+        console.log('经纬度', lng, lat, props.checkList, model.value)
+
+        dom.onmousemove = null
+        dom.onmouseup = null
+        ismove = false
+      }
+    }
+  }
+}
+
+// 初始化
+async function initMap(callback:()=>void) {
+  // store.setLoading(true)
+  // init
+  viewer = new Viewer('cesiumContainer', {
+    baseLayerPicker: false,
+    geocoder: false,
+    infoBox: false,
+    homeButton: false,
+    sceneModePicker: false,
+    navigationHelpButton: false,
+    animation: false,
+    creditContainer: 'cesiumContainer',
+    timeline: false,
+    fullscreenButton: false,
+    vrButton: false,
+    requestRenderMode: true,
+    scene3DOnly: true,
+    selectionIndicator: false,
+    navigationInstructionsInitiallyVisible: false
+    // sceneMode: SceneMode.COLUMBUS_VIEW
+  })
+  // 增加地形属性
+  viewer.scene.globe.depthTestAgainstTerrain = true
+  // 缩放范围
+  viewer.scene.screenSpaceCameraController.minimumZoomDistance = 100
+  viewer.scene.screenSpaceCameraController.maximumZoomDistance = 7546388
+  // viewer.imageryLayers.removeAll(true)
+  // viewer.imageryLayers.get(0).show = false// 不显示底图
+  // viewer.scene.globe.baseColor = Color.fromCssColorString('#07101a') // 设置地球颜色07101a
+  viewer.imageryLayers.addImageryProvider(new UrlTemplateImageryProvider({
+    url: 'https://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8'
+  }))
+  // loadGeoJson('are.json', 1, 7546388)
+  // loadGeoJson('road.json', 1, 7546388)
+  // 加载图层
+  // viewer.imageryLayers.addImageryProvider(new UrlTemplateImageryProvider({
+  //   url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
+  //   subdomains: [ '0', '1', '2', '3' ],
+  //   tilingScheme: new WebMercatorTilingScheme(),
+  //   maximumLevel: 14
+  // }))
+
+  // 加载地形
+  // viewer.terrainProvider = await (CesiumTerrainProvider as any).fromUrl('https://www.supermapol.com/realspace/services/3D-stk_terrain/rest/realspace/datas/info/data/path', {
+  //   requestMetadata: true,
+  //   requestVertexNormals: true,
+  //   requestWaterMask: true
+  // })
+
+  viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(ScreenSpaceEventType.LEFT_DOUBLE_CLICK) // 取消原双击事件
+  viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK) // 取消原单击事件
+
+  // viewer.camera.changed.addEventListener(() => {
+  //   // 打印中心点坐标、高度
+  //   const { height } = viewer.scene.globe.ellipsoid.cartesianToCartographic(viewer.camera.position)
+  //   console.log(999999, height)
+  // })
+
+  // 重新注册点击事件
+  handler = new ScreenSpaceEventHandler(viewer.scene.canvas)
+
+  // 右键还原
+  handler.setInputAction(() => {
+    viewer.camera.flyTo({
+      destination: Cartesian3.fromDegrees(101.84599, 30.04260, 7546388),
+      orientation: {
+        heading: CMath.toRadians(0.0),
+        pitch: CMath.toRadians(-85.0),
+        roll: CMath.toRadians(0.0)
+      }
+    })
+  }, ScreenSpaceEventType.RIGHT_CLICK)
+
+  // 点击左键
+  handler.setInputAction((movement: Any) => {
+    const pick = viewer.scene.pick(movement.position)
+    if (!isAdmin.value || !pick) return
+    if (isAdd.value) {
+      notification.warning({
+        content: '请完成你的操作再来!',
+        duration: 3000
+      })
+      return
+    }
+    console.log('点击详情', pick.id)
+    isAdd.value = false
+    const { oldData } = pick.id
+    model.value = { ...oldData, selfId: pick.id.id }
+    modelShow.value = true
+  }, ScreenSpaceEventType.LEFT_CLICK)
+
+  // 监听加载结束
+  Helper.add(viewer.scene.globe.tileLoadProgressEvent, (e: any) => {
+    if (e === 0) {
+      viewer.camera.flyTo({
+        destination: Cartesian3.fromDegrees(101.84599, 30.04260, 7546388),
+        orientation: {
+          heading: CMath.toRadians(0.0),
+          pitch: CMath.toRadians(-85.0),
+          roll: CMath.toRadians(0.0)
+        }
+      })
+      // store.setLoading(false)
+      Helper.removeAll()
+      // 初始化添加标点
+      callback()
+    }
+  })
+}
+
+function close() {
+  if (isAdd.value) {
+    const { values } = viewer.entities
+    if (values.length) viewer.entities.removeById(values[values.length - 1].id)
+    viewer.scene.requestRender()
+    isAdd.value = false
+  }
+  modelShow.value = false
+}
+
+async function submit() {
+  await formRef.value.validate()
+  // 提交数据
+  const msg = isAdd.value ? await equipmentService.addDevice({ ...model.value }) : await equipmentService.upDevice({ ...model.value })
+  notification.success({
+    content: msg,
+    duration: 3000
+  })
+  // 属性赋值
+  const { values } = viewer.entities
+  values[values.length - 1].oldData = { ...model.value, selfId: values[values.length - 1].id }
+  modelShow.value = false
+  isAdd.value = false
+}
+
+async function del() {
+  dialog.info({
+    title: '你确定要删除当前设备?',
+    positiveText: '确定',
+    onPositiveClick: async () => {
+      const msg = await equipmentService.delDevice({ ...model.value })
+      notification.success({
+        content: msg,
+        duration: 3000
+      })
+      viewer.entities.removeById(model.value.selfId)
+      viewer.scene.requestRender()
+      modelShow.value = false
+    }
+  })
+}
+
+/** 递归 */
+function recursion(arr: Array<TreeList>, newData: Array<TreeList>) {
+  for (let k = 0; k < arr.length; k++) {
+    const el = arr[k]
+    if (el.lastData === 1 || el.lastData === 0) {
+      newData.push({
+        name: el.name,
+        id: el.id,
+        lastData: el.lastData,
+        itemLongitude: el.itemLongitude,
+        itemLatitude: el.itemLatitude,
+        deviceDataList: el.deviceDataList
+      })
+    }
+    if (el.children && el.children.length) recursion(el.children, newData)
+  }
+}
+/** input 正则 */
+function onlyAllowNumber(value:string, type:boolean) {
+  if (type) {
+    return +(value) >= 0 && +(value) <= 180
+  }
+  return +(value) >= 0 && +(value) <= 90
+}
+// 选中时
+watch(() => props.checkList, (v) => {
+  if (!isAdmin.value) return
+  if (!v.length) {
+    viewer.entities.removeAll()
+  } else {
+    equipmentService.getDevice(v[0]?.id as number).then((res) => {
+      viewer.entities.removeAll()
+      for (let k = 0; k < res.length; k++) {
+        const el = res[k]
+        if (el.del === 1) {
+          const items = icontypes.find((es) => es.deviceType === el.deviceType)
+          addLable(+(el.longitude!), +(el.latitude!), items!.icon, items!.name, 1, 16219740, el)
+        }
+      }
+      viewer.scene.requestRender()
+    })
+  }
+  viewer.scene.requestRender()
+  close()
+})
+
+// 树列表
+watch(() => props.treeList, (v) => {
+  options.value = v
+  viewer.entities.removeAll()
+  if (isAdmin.value || !v.length) return
+  const newData = [] as TreeList[]
+  recursion(v, newData)
+  for (let k = 0; k < newData.length; k++) {
+    const el = newData[k]
+    if (el.lastData === 1) {
+      // 项目
+      if (el.itemLongitude) {
+        const lngLat = el.itemLongitude?.split(',')
+        addLable(+(lngLat![0]), +(lngLat![1]), lnglatIcon, el.nameReferred || el.name, 1038633, 4938633)
+      }
+      if (el.deviceDataList && el.deviceDataList.length) {
+        // 设备
+        for (let j = 0; j < el.deviceDataList.length; j++) {
+          const es = el.deviceDataList[j]
+          if (es.del === 1) {
+            const items = icontypes.find((ed) => ed.deviceType === es.deviceType)
+            addLable(+(es.longitude!), +(es.latitude!), items!.icon, items!.name, 1, 938633)
+          }
+        }
+      }
+    } else {
+      // 公司
+    }
+  }
+  viewer.scene.requestRender()
+})
+
+// 修改经纬度=>同步图标
+watch(() => [ model.value.longitude, model.value.latitude ], (v) => {
+  if (isAdd.value) return
+  const ens = viewer.entities.getById(model.value.selfId)
+  ens.position = Cartesian3.fromDegrees(+v[0], +v[1])
+  setTimeout(() => {
+    viewer.scene.requestRender()
+  }, 1500)
+})
+
+onMounted(() => {
+  initMap(() => {
+    if (isAdmin.value) initMove()
+    // loadGeoJson('geo.json')
+    // loadGeoJson('1.geojson')
+    // loadGeoJson('2.geojson')
+    // loadGeoJson('3.geojson')
+    // loadGeoJson('4.geojson')
+    // loadGeoJson('5.geojson')
+    // loadGeoJson('6.geojson')
+
+    // loadGeoJson('gm.geojson')
+    // loadGeoJson('tmy.geojson')
+    // loadGeoJson('dd.geojson')
+  })
+})
+
+onUnmounted(() => {
+  // store.setLoading(false)
+  if (viewer) {
+    if (viewer.entities) viewer.entities.removeAll()
+    viewer.destroy()
+  }
+  viewer = null
+  Helper = null
+  handler = null
+  const dom = (document.querySelector('.maps') as HTMLElement)
+  if (dom && dom.onmousedown) dom.onmousedown = null
+})
+</script>
+<template>
+  <div class="maps">
+    <div id="cesiumContainer" />
+    <template v-if="isAdmin">
+      <div class="topbar">
+        <template
+          v-for="(item, index) in icontypes"
+          :key="index"
+        >
+          <div>
+            <Icon
+              :name="item.type"
+              :size="50"
+              :data-type="item.type"
+            />
+            <p>{{ item.name }}</p>
+          </div>
+        </template>
+      </div>
+      <Icon
+        :name="currenIcon.icon"
+        :size="25"
+        class="moveIcon"
+        :style="`left:${currenIcon.x};top:${currenIcon.y}`"
+      />
+      <div
+        v-if="modelShow"
+        class="details"
+      >
+        <n-card
+          style="width: 400px"
+          :bordered="false"
+          size="huge"
+          role="dialog"
+          aria-modal="true"
+          class="n-modal"
+        >
+          <n-form
+            ref="formRef"
+            :model="model"
+            :rules="rules"
+          >
+            <n-form-item
+              path="deviceId"
+              label="设备ID"
+            >
+              <n-input
+                v-model:value="model.deviceId"
+                :readonly="!isAdd"
+              />
+            </n-form-item>
+            <n-form-item
+              path="itemId"
+              label="所属项目"
+            >
+              <n-tree-select
+                v-model:value="model.itemId"
+                :options="options"
+                :render-label="(info:any)=>info.option.nameReferred || info.option.name"
+                label-field="name"
+                key-field="id"
+                check-strategy="child"
+                clearable
+                filterable
+              />
+            </n-form-item>
+            <n-form-item
+              path="longitude"
+              label="经度"
+            >
+              <n-input
+                v-model:value="model.longitude"
+                :allow-input="(e: string) => onlyAllowNumber(e, true)"
+              />
+            </n-form-item>
+            <n-form-item
+              path="latitude"
+              label="纬度"
+            >
+              <n-input
+                v-model:value="model.latitude"
+                :allow-input="(e: string) => onlyAllowNumber(e, false)"
+              />
+            </n-form-item>
+            <n-form-item
+              path="describeData"
+              label="安装位置描述"
+            >
+              <n-input
+                v-model:value="model.describeData"
+                type="textarea"
+                size="small"
+                :autosize="{
+                  minRows: 3,
+                  maxRows: 5
+                }"
+                :maxlength="150"
+              />
+            </n-form-item>
+          </n-form>
+          <template #footer>
+            <n-button @click="close">
+              取消
+            </n-button>
+            <n-button
+              v-if="!isAdd"
+              type="error"
+              @click="del"
+            >
+              删除
+            </n-button>
+            <n-button
+              type="primary"
+              @click="submit"
+            >
+              确定
+            </n-button>
+          </template>
+        </n-card>
+      </div>
+    </template>
+  </div>
+</template>
+<style scoped lang="scss">
+.maps {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+
+  #cesiumContainer {
+    width: 100%;
+    height: 100%;
+  }
+
+  .topbar {
+    position: absolute;
+    top: 10px;
+    left: 50%;
+    width: 1080px;
+    height: 104px;
+    transform: translate(-50%, 0);
+    background: linear-gradient(229deg, rgba(26, 65, 78, 0.85) 0%, rgba(3, 20, 26, 0.95) 100%);
+    border: 1px solid rgba(105, 206, 206, 0.8);
+    display: flex;
+    align-items: center;
+
+    div {
+      display: flex;
+      flex-flow: column;
+      align-items: center;
+      width: 65px;
+      user-select: none;
+      cursor: pointer;
+      font-size: 14px;
+
+      &:first-child {
+        margin-left: 10px;
+      }
+
+      p {
+        margin-top: 5px;
+        text-align: center;
+      }
+    }
+
+  }
+
+  .moveIcon {
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+
+  .details {
+    position: absolute;
+    right: 10px;
+    top: 10px;
+    min-height: 400px;
+  }
+}
+</style>