Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	src/types/components.d.ts
yangyuxuan 5 mesi fa
parent
commit
8a05c70935

+ 9 - 0
src/api/videoMonitor/index.ts

@@ -89,3 +89,12 @@ export function getLxHyVideoTagInfo(params) {
     params: params
   });
 }
+
+// 全域地图-视频点位信息
+export function getVideoList(params) {
+  return request({
+    url: '/api/videoResource/videoinfo/get_video_list',
+    method: 'get',
+    params: params
+  });
+}

BIN
src/assets/images/arrow.png


+ 2 - 1
src/components/NearbyVideos/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog custom-show type="lg" title="附近视频" height="720px" draggable hide-footer @close="handleClose">
+  <Dialog custom-show type="lg" :title="!!title ? title : '附近视频'" height="720px" draggable hide-footer @close="handleClose">
     <div class="flex-box">
       <div>附近距离</div>
       <el-select v-model="queryParams.radius" class="custom-select" popper-class="custom-popper" :teleported="false" @change="initData">
@@ -31,6 +31,7 @@ interface Props {
   modelValue: boolean;
   location?: string | number[];
   getDataMethod?: Function;
+  title?: string;
 }
 const props = withDefaults(defineProps<Props>(), {});
 const emits = defineEmits(['update:modelValue']);

+ 295 - 59
src/components/TimeAxis/index.vue

@@ -35,7 +35,7 @@
         <div class="prevIcon" @click="toPrevTime" />
         <div class="playIcon" @click="toPlayTime" />
         <div class="nextIcon" @click="toNextTime(true)" />
-        <div class="speed-btn" @click="changeSpeed">{{ timeAxisState.speed === 1 ? 'x1' : 'x2' }}</div>
+        <div class="speed-btn" @click="changeSpeed">{{ timeAxisState.speed === 2 ? 'x1' : 'x2' }}</div>
         <div class="rankIcon" />
       </div>
     </div>
@@ -45,6 +45,15 @@
 <script lang="ts" setup name="TimeAxis">
 import { parseTime } from '@/utils/ruoyi';
 import carImg from '@/assets/images/car.png';
+import VectorSource from 'ol/source/Vector';
+import Feature from 'ol/Feature';
+import Point from 'ol/geom/Point';
+import Icon from 'ol/style/Icon';
+import Style from 'ol/style/Style';
+import { LineString } from 'ol/geom';
+import VectorLayer from 'ol/layer/Vector';
+import { Stroke } from 'ol/style';
+import arrowImg from '@/assets/images/arrow.png';
 
 const props = defineProps({
   activeMap: String
@@ -69,7 +78,7 @@ const timeAxisState = reactive({
   playing: false,
   show: false,
   showDialog: false,
-  speed: 1,
+  speed: 2,
   data: []
 });
 let originalData = [];
@@ -129,34 +138,230 @@ const initDataToPlay = (obj) => {
   if (timeAxisState.type === 'track') {
     mapUtils = getMapUtils();
     map = mapUtils?.getMap();
-    AMap = mapUtils?.getAMap();
     if (AMapType.includes(props.activeMap)) {
-      if (moveMarker) {
-        // 不停止remove还是有
-        moveMarker.stopMove();
-        moveMarker.remove();
-      }
-      movePolyline?.remove();
-      movePassedPolyline?.remove();
-      // 绘制轨迹
-      movePolyline = new AMap.Polyline({
-        map: map,
-        path: trackPathArr,
-        showDir: true,
-        strokeColor: '#28F', //线颜色
-        // strokeOpacity: 1,     //线透明度
-        strokeWeight: 6 //线宽
-        // strokeStyle: "solid"  //线样式
-      });
-
-      movePassedPolyline = new AMap.Polyline({
-        map: map,
-        strokeColor: '#AF5', //线颜色
-        strokeWeight: 6 //线宽
-      });
-      startPlay();
+      AMap = mapUtils?.getAMap();
+      trackPlayback1();
+    } else {
+      trackPlayback2();
+    }
+  }
+};
+// 高德地图轨迹
+const trackPlayback1 = () => {
+  clearObj();
+  // 绘制轨迹
+  movePolyline = new AMap.Polyline({
+    map: map,
+    path: trackPathArr,
+    showDir: true,
+    strokeColor: '#28F', //线颜色
+    // strokeOpacity: 1,     //线透明度
+    strokeWeight: 6, //线宽
+    // strokeStyle: "solid"  //线样式
+  });
+
+  movePassedPolyline = new AMap.Polyline({
+    map: map,
+    strokeColor: '#AF5', //线颜色
+    strokeWeight: 6 //线宽
+  });
+
+  startPlay();
+};
+
+let carLayer, carFeature, traceFeature, traceFeature2;
+let lastTime;
+let distance = 0;
+let animationFlag = false;
+let route,
+  lastRoute = [];
+let first = 0;
+
+const trackPlayback2 = () => {
+  if (!!carLayer) {
+    clearObj();
+  } else {
+    const source = new VectorSource();
+    carLayer = new VectorLayer({
+      source: source
+    });
+    map.addLayer(carLayer);
+  }
+
+  const angle = getAngle(trackPathArr[0], trackPathArr[1]);
+  lastTime = Date.now();
+  carFeature = new Feature({
+    geometry: new Point(trackPathArr[0]),
+    zIndex: 999
+  });
+  const icon = new Icon({
+    crossOrigin: 'anonymous',
+    src: carImg,
+    width: 13,
+    height: 26,
+    anchor: [0.5, 0.5],
+    rotation: angle
+  });
+  carFeature.setStyle(
+    new Style({
+      image: icon
+    })
+  );
+  route = new Feature({
+    geometry: new LineString(trackPathArr[0])
+  });
+  traceFeature = new Feature({
+    geometry: new LineString(trackPathArr),
+    zIndex: 8
+  });
+  traceFeature.setStyle(arrowStyleFunction);
+  traceFeature2 = new Feature({
+    geometry: new LineString(trackPathArr[0]),
+    zIndex: 9
+  });
+  lastRoute = [trackPathArr[0]];
+  traceFeature2.setStyle(
+    new Style({
+      stroke: new Stroke({
+        color: '#AF5',
+        width: 6
+      })
+    })
+  );
+  carLayer.getSource().addFeatures([carFeature, traceFeature, traceFeature2]);
+  carLayer.on('postrender', move);
+  timeAxisState.activeIndex = 0;
+  timeAxisState.playing = true;
+  // 触发地图渲染
+  const geo = carFeature.getGeometry().clone();
+  carFeature.setGeometry(geo);
+};
+const move = (e) => {
+  if (!timeAxisState.playing) return;
+  const time = e.frameState.time;
+  // 时间戳差(毫秒)
+  const elapsedTime = time - lastTime;
+  // 距离(其实是比例的概念)
+  distance = distance + ((timeAxisState.speed === 2 ? 1 : 2) * 200 * elapsedTime) / 1e6;
+  if (distance >= 1) {
+    if (timeAxisState.activeIndex < trackPathArr.length - 1) {
+      // 一段走完后
+      lastRoute.push(traceFeature2.getGeometry().getCoordinates());
+      console.log(lastRoute);
+      timeAxisState.activeIndex += 1;
+      distance = 0;
+    } else if (first === 0) {
+      // 所有走完后有时会0.9->1.2导致路没走完
+      first = 1;
+      distance = 1;
+    } else {
+      first = 0;
+      distance = 0;
+      animationFlag = false;
+      stopAnimation();
+      return;
     }
   }
+  const arr = trackPathArr.slice(timeAxisState.activeIndex, timeAxisState.activeIndex + 2);
+  route = new Feature({
+    geometry: new LineString(arr)
+  });
+  // 保存当前时间
+  lastTime = time;
+  // 上次坐标
+  const lastCoord = carFeature.getGeometry().getCoordinates();
+  // 获取新位置的坐标点
+  let curCoord = route.getGeometry().getCoordinateAt(distance);
+  if (isNaNCoord(curCoord)) {
+    curCoord = trackPathArr[0];
+  }
+  // 设置新坐标
+  carFeature.getGeometry().setCoordinates(curCoord);
+  // 获取当前轨迹坐标数组
+  const currentCoords = traceFeature2.getGeometry().getCoordinates();
+  currentCoords.push(curCoord);
+  traceFeature2.getGeometry().setCoordinates(currentCoords);
+  map.getView().setCenter(curCoord);
+  // 设置角度
+  carFeature.getStyle().getImage().setRotation(getAngle(lastCoord, curCoord));
+  // 调用地图渲染
+  map.render();
+};
+const isNaNCoord = (coord) => {
+  return JSON.stringify(coord) === '[null,null]'; // NaN 在 JSON.stringify 中会被转换为 null
+};
+const stopAnimation = () => {
+  if (carLayer) {
+    carLayer.un('postrender', move);
+  }
+};
+const getAngle = (point1, point2) => {
+  // 处理无效输入或非数组情况
+  if (!point1 || !point2 || !point1.length || !point2.length) {
+    return 0;
+  }
+
+  const dx = point2[0] - point1[0];
+  const dy = point2[1] - point1[1];
+
+  // 处理两点相同的情况
+  if (dx === 0 && dy === 0) {
+    return 0;
+  }
+
+  // 使用 Math.atan2 计算角度,并确保结果在 0 到 2π 之间
+  let angle = Math.atan2(dx, dy);
+  if (angle < 0) {
+    angle += 2 * Math.PI;
+  }
+
+  return angle;
+};
+// 箭头样式生成函数
+const arrowStyleFunction = (feature, resolution) => {
+  const geometry = feature.getGeometry();
+  const styles = [
+    // 基础线样式
+    new Style({
+      stroke: new Stroke({
+        color: '#2196F3',
+        width: 6
+      })
+    })
+  ];
+
+  // 计算箭头间隔(每50像素一个箭头)
+  const length = geometry.getLength();
+  const pixelInterval = 50;
+  const geoInterval = pixelInterval * resolution;
+  const arrowCount = Math.floor(length / geoInterval);
+
+  // 生成箭头样式
+  for (let i = 0; i <= arrowCount; i++) {
+    const ratio = i / arrowCount;
+    const coord = geometry.getCoordinateAt(ratio);
+
+    // 获取线段方向
+    const prevCoord = geometry.getCoordinateAt(Math.max(0, ratio - 0.001));
+    const dx = coord[0] - prevCoord[0];
+    const dy = coord[1] - prevCoord[1];
+    const rotation = Math.atan2(dy, dx);
+
+    // styles.push(
+    //   new Style({
+    //     geometry: new Point(coord),
+    //     image: new Icon({
+    //       src: arrowImg, // 替换为实际箭头图标路径
+    //       anchor: [0.75, 0.5], // 图标锚点
+    //       rotateWithView: true, // 随地图旋转
+    //       rotation: -rotation, // 方向校准
+    //       scale: 0.05
+    //     }),
+    //     zIndex: 10
+    //   })
+    // );
+  }
+  return styles;
 };
 // 创建新标点 moveTo调用第二次不生效,重新生成marker
 const createMarker = () => {
@@ -215,6 +420,13 @@ const playOrPauseTrack = () => {
       moveMarker.pauseMove();
     }
     isNewMarkerPause = false;
+  } else {
+    if (!!timeAxisState.playing) {
+      lastTime = Date.now();
+      // 触发地图渲染
+      const geo = carFeature.getGeometry().clone();
+      carFeature.setGeometry(geo);
+    }
   }
 };
 // 轨迹开始播放
@@ -230,19 +442,41 @@ const prevPauseTime = () => {
   // 更新索引
   timeAxisState.playing = false;
   timeAxisState.activeIndex -= 1;
-  moveMarker.stopMove();
-  createMarker();
-  movePassedPolyline.setPath(trackPathArr.slice(0, timeAxisState.activeIndex + 1));
-  isNewMarkerPause = true;
+  if (AMapType.includes(props.activeMap)) {
+    moveMarker.stopMove();
+    createMarker();
+    movePassedPolyline.setPath(trackPathArr.slice(0, timeAxisState.activeIndex + 1));
+    isNewMarkerPause = true;
+  } else {
+    // 设置新坐标
+    const curCoord = trackPathArr[timeAxisState.activeIndex];
+    carFeature.getGeometry().setCoordinates(curCoord);
+    // 获取当前轨迹坐标数组
+    const currentCoords = lastRoute[timeAxisState.activeIndex];
+    lastRoute = lastRoute.slice(0, timeAxisState.activeIndex + 1);
+    traceFeature2.getGeometry().setCoordinates(currentCoords);
+    map.getView().setCenter(curCoord);
+  }
 };
 const nextPauseTime = () => {
   if (timeAxisState.activeIndex <= 0) return;
   // 更新索引
   timeAxisState.playing = false;
   timeAxisState.activeIndex += 1;
-  createMarker();
-  movePassedPolyline.setPath(trackPathArr.slice(0, timeAxisState.activeIndex + 1));
-  isNewMarkerPause = true;
+  if (AMapType.includes(props.activeMap)) {
+    createMarker();
+    movePassedPolyline.setPath(trackPathArr.slice(0, timeAxisState.activeIndex + 1));
+    isNewMarkerPause = true;
+  } else {
+    // 设置新坐标
+    const curCoord = trackPathArr[timeAxisState.activeIndex];
+    carFeature.getGeometry().setCoordinates(curCoord);
+    // 获取当前轨迹坐标数组
+    const currentCoords = lastRoute[lastRoute.length - 1].concat([curCoord]);
+    lastRoute.push(currentCoords);
+    traceFeature2.getGeometry().setCoordinates(currentCoords);
+    map.getView().setCenter(curCoord);
+  }
 };
 // 轨迹播放下一个节点
 const playNextNode = () => {
@@ -258,31 +492,6 @@ const playNextNode = () => {
     duration: timeAxisState.speed === 1 ? 2000 : 1000
   });
 };
-//传入两个经纬度点得到车辆角度 设置车辆Marker角度
-const getAngle = (startPoint, endPoint) => {
-  if (!(startPoint && endPoint)) {
-    return 0;
-  }
-
-  let dRotateAngle = Math.atan2(
-    Math.abs(startPoint.lng - endPoint.lng),
-    Math.abs(startPoint.lat - endPoint.lat)
-  );
-  if (endPoint.lng >= startPoint.lng) {
-    if (endPoint.lat >= startPoint.lat) {
-    } else {
-      dRotateAngle = Math.PI - dRotateAngle;
-    }
-  } else {
-    if (endPoint.lat >= startPoint.lat) {
-      dRotateAngle = 2 * Math.PI - dRotateAngle;
-    } else {
-      dRotateAngle = Math.PI + dRotateAngle;
-    }
-  }
-  dRotateAngle = (dRotateAngle * 180) / Math.PI;
-  return dRotateAngle;
-};
 
 // 滚动到可见到当前点位置
 watch(
@@ -301,10 +510,37 @@ watch(
   }
 );
 
+// 清除元素
+const clearObj = () => {
+  if (AMapType.includes(props.activeMap)) {
+    if (moveMarker) {
+      // 不停止remove还是有
+      moveMarker.stopMove();
+      moveMarker.remove();
+    }
+    movePolyline?.remove();
+    movePassedPolyline?.remove();
+  } else {
+    if (!!carFeature) {
+      carLayer.getSource().removeFeature(carFeature);
+    }
+    if (!!traceFeature) {
+      carLayer.getSource().removeFeature(traceFeature);
+    }
+    if (!!traceFeature2) {
+      carLayer.getSource().removeFeature(traceFeature2);
+    }
+  }
+};
+
 onMounted(() => {
   getInitTime();
 });
 
+onUnmounted(() => {
+  clearObj();
+});
+
 defineExpose({ initDataToPlay });
 </script>
 

+ 32 - 0
src/types/components.d.ts

@@ -24,14 +24,24 @@ declare module 'vue' {
     DistributionMap: typeof import('./../components/Map/YztMap/DistributionMap.vue')['default']
     DrawMap: typeof import('./../components/Map/YztMap/DrawMap.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
+    ElAnchor: typeof import('element-plus/es')['ElAnchor']
+    ElAnchorLink: typeof import('element-plus/es')['ElAnchorLink']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElCollapse: typeof import('element-plus/es')['ElCollapse']
+    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -41,26 +51,45 @@ declare module 'vue' {
     ElEmpty: typeof import('element-plus/es')['ElEmpty']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+    ElLink: typeof import('element-plus/es')['ElLink']
+    ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
+    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+    ElSegmented: typeof import('element-plus/es')['ElSegmented']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
+    ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
     ElSlider: typeof import('element-plus/es')['ElSlider']
+    ElSpace: typeof import('element-plus/es')['ElSpace']
+    ElStep: typeof import('element-plus/es')['ElStep']
+    ElSteps: typeof import('element-plus/es')['ElSteps']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
     ElTimeline: typeof import('element-plus/es')['ElTimeline']
     ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
+    ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
+    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
     ElUpload: typeof import('element-plus/es')['ElUpload']
     ExcelEditor: typeof import('./../components/ExcelEditor/index.vue')['default']
     FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
@@ -72,6 +101,9 @@ declare module 'vue' {
     HikvisionPlayer: typeof import('./../components/HKVideo/hikvision-player.vue')['default']
     HKVideo: typeof import('./../components/HKVideo/index.vue')['default']
     IconSelect: typeof import('./../components/IconSelect/index.vue')['default']
+    IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
+    IEpCaretTop: typeof import('~icons/ep/caret-top')['default']
+    IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default']
     IFrame: typeof import('./../components/iFrame/index.vue')['default']
     ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
     ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']

+ 11 - 16
src/views/emergencyCommandMap/LeftSection/VideoMonitorEdit.vue

@@ -73,7 +73,7 @@
 </template>
 
 <script lang="ts" setup>
-import { getEmergencyVideoCata, getUserVideoPoints, updateUserVideoPoints } from '@/api/videoMonitor';
+import { getEmergencyVideoCata, getUserVideoPoints, getVideoList, updateUserVideoPoints } from '@/api/videoMonitor';
 import { deepClone } from '@/utils';
 import useAppStore from '@/store/modules/app';
 import useMapStore from '@/store/modules/map';
@@ -144,20 +144,11 @@ const getList = () => {
       filterData(editData.value);
     }
   });
-  // 模拟分类数据
-  if (!!queryParams.dict_value) {
-    const data = [
-      {
-        "name": "桂NC0227",
-        "id": "桂NC0227",
-        "dataType": 43,
-        "longitude": "110.35366599999999",
-        "latitude": "22.030815999999998",
-        "type": "2"
-      }
-    ];
-    mapStore.setTrackState(data);
-  }
+};
+const getVideoInfoList = () => {
+  getVideoList(queryParams).then((res) => {
+    mapStore.setTrackState(res.data);
+  });
 };
 const selectItem = (item) => {
   if (editVideo.value) {
@@ -186,6 +177,7 @@ const reset = () => {
 const handleQuery = () => {
   queryParams.current = 1;
   getList();
+  // getVideoInfoList();
 };
 
 /** 重置按钮操作 */
@@ -228,7 +220,10 @@ const handleSave = () => {
   });
 };
 
-getList();
+onMounted(() => {
+  getList();
+  // getVideoInfoList();
+});
 </script>
 
 <style lang="scss" scoped>

+ 1 - 1
src/views/globalMap/RightMenu/KeyVehicles.vue

@@ -88,7 +88,7 @@ const handleCollaborate = () => {};
 const handleTrack = (item) => {
   getVehicleTrajectory(item.vehicle_no).then((res) => {
     const trajectory = [];
-    res.rows.forEach((item) => {
+    res.data.list.forEach((item) => {
       trajectory.push({
         time: !!item.gpsDate ? parseTime(item.gpsDate, '{h}:{i}') : '',
         lnglat: [item.lng, item.lat]

+ 5 - 9
src/views/globalMap/RightMenu/TyphoonVideo.vue

@@ -49,11 +49,7 @@
         </div>
       </div>
     </div>
-    <Dialog v-if="showDialog" v-model="showDialog" draggable type="md" title="台风视频" hide-footer>
-      <div style="width: 100%; height: 700px; display: flex; justify-content: center; align-items: center">
-        <HKVideo :dot_data="videoMonitorData" />
-      </div>
-    </Dialog>
+    <NearbyVideos v-if="showDialog" v-model="showDialog" :location="location" title="周边视频" />
   </div>
 </template>
 
@@ -76,10 +72,10 @@ onMounted(() => {
   initData();
 });
 let showDialog = ref(false);
-let videoMonitorData = ref({});
-const handleConnect = (index: number, item: any) => {
-  videoMonitorData.value = item; // 将当前行数据赋值给videoMonitorData
-  showDialog.value = true; // 显示弹窗
+let location = ref([]);
+const handleConnect = (item: any) => {
+  location.value = [item.longitude, item.latitude];
+  showDialog.value = true;
 };
 const handleYearChange = async () => {
   if (selectedYear.value) {