Browse Source

鼠标悬停显示台风信息

WQQ 1 month ago
parent
commit
1558d1468a
1 changed files with 297 additions and 127 deletions
  1. 297 127
      WebVue/TaiHufenglang/src/components/TyphoonViewer.vue

+ 297 - 127
WebVue/TaiHufenglang/src/components/TyphoonViewer.vue

@@ -1,20 +1,20 @@
 <template>
   <div id="cesiumContainer" style="height: 100%;width: 100%;"></div>
 
-    <!-- 控制按钮 -->
-    <div class="control-buttons">
-      <button class="control-btn" @click="goToHomeView" title="返回首页视角">
-        <i class="fas fa-home"></i>
-      </button>
-      <button class="control-btn" @click="togglePOIDisplay" title="显示/隐藏POI点">
-        <i class="fas fa-map-marker-alt" :class="{ 'active': poiVisible }"></i>
-      </button>
-      <button class="control-btn" @click="toggleTyphoon" title="显示/隐藏台风并切换视角">
-        <i class="fas fa-wind" :class="{ 'active': typhoonVisible }"></i>
-      </button>
-    </div>
+  <!-- 控制按钮 -->
+  <div class="control-buttons">
+    <button class="control-btn" @click="goToHomeView" title="返回首页视角">
+      <i class="fas fa-home"></i>
+    </button>
+    <button class="control-btn" @click="togglePOIDisplay" title="显示/隐藏POI点">
+      <i class="fas fa-map-marker-alt" :class="{ 'active': poiVisible }"></i>
+    </button>
+    <button class="control-btn" @click="toggleTyphoon" title="显示/隐藏台风并切换视角">
+      <i class="fas fa-wind" :class="{ 'active': typhoonVisible }"></i>
+    </button>
+  </div>
 
-  <!-- 自定义弹框组件 -->
+  <!-- 自定义弹框组件 - POI点 -->
   <div v-if="selectedPoint" class="custom-popup" :style="{
     left: `${popupPosition.x}px`,
     top: `${popupPosition.y}px`
@@ -26,9 +26,28 @@
       <div class="popup-arrow"></div>
     </div>
   </div>
-  
-  <!-- 台风图例面板 -->
-  <ul class="legend">
+
+  <!-- 台风路径点信息弹窗 -->
+  <div v-if="typhoonInfoVisible && typhoonVisible" class="typhoon-popup" :style="{
+    left: `${typhoonPopupPosition.x}px`,
+    top: `${typhoonPopupPosition.y}px`
+  }">
+    <div class="popup-content">
+      <h3>{{ typhoonInfo.name }}({{ typhoonInfo.enname }})</h3>
+      <p>{{ typhoonInfo.time }}</p>
+      <p><strong>中心位置:</strong> {{ typhoonInfo.lng }}° / {{ typhoonInfo.lat }}°</p>
+      <p><strong>风速风力:</strong> {{ typhoonInfo.speed }} m/s ({{ typhoonInfo.power }}级)</p>
+      <p><strong>中心气压:</strong> {{ typhoonInfo.pressure }} hPa</p>
+      <p><strong>移速移向:</strong> {{ typhoonInfo.movedirection }} {{ typhoonInfo.movespeed }} km/h</p>
+      <p><strong>七级半径:</strong> {{ typhoonInfo.radius7 || '--' }}</p>
+      <p><strong>十级半径:</strong> {{ typhoonInfo.radius10 || '--' }}</p>
+      <p><strong>十二级半径:</strong> {{ typhoonInfo.radius12 || '--' }}</p>
+      <div class="popup-arrow"></div>
+    </div>
+  </div>
+
+  <!-- 台风图例面板 - 添加v-if控制显示 -->
+  <ul class="legend" v-if="typhoonVisible">
     <li>
       <span class="dot green"></span>
       <span>热带低压</span>
@@ -53,7 +72,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted, watch } from 'vue'
+import { ref, onMounted, onUnmounted } from 'vue'
 import * as Cesium from 'cesium';
 import "cesium/Build/CesiumUnminified/Widgets/widgets.css";
 import JYLData from '@/assets/Data/THJYL.json'
@@ -66,6 +85,12 @@ const popupPosition = ref({ x: 0, y: 0 });
 let handler = null;
 let viewer = null;
 
+// 新增台风信息弹窗变量
+const typhoonInfoVisible = ref(false);
+const typhoonInfo = ref({});
+const typhoonPopupPosition = ref({ x: 0, y: 0 });
+const typhoonPointDataMap = ref(new Map()); // 存储台风点与数据的映射
+
 // 控制按钮相关变量
 const poiVisible = ref(true); // POI点显示状态
 const typhoonVisible = ref(true); // 台风显示状态
@@ -120,45 +145,50 @@ const togglePOIDisplay = () => {
 // 切换台风显示/隐藏并跳转视角
 const toggleTyphoon = () => {
   typhoonVisible.value = !typhoonVisible.value;
-  
+
   // 控制台风相关实体显示/隐藏
   if (myEntityCollection.value) {
     myEntityCollection.value.show = typhoonVisible.value;
   }
-  
+
   // 控制风圈显示/隐藏
   fengquanLayers.value.forEach(entity => {
     if (entity) {
       entity.show = typhoonVisible.value;
     }
   });
-  
+
   // 控制路径线显示/隐藏
   typhoonRelatedEntities.value.paths.forEach(entity => {
     if (entity) {
       entity.show = typhoonVisible.value;
     }
   });
-  
+
   // 控制预报路径显示/隐藏
   typhoonRelatedEntities.value.forecasts.forEach(entity => {
     if (entity) {
       entity.show = typhoonVisible.value;
     }
   });
-  
+
   // 控制警戒线显示/隐藏
   typhoonRelatedEntities.value.warnings.forEach(entity => {
     if (entity) {
       entity.show = typhoonVisible.value;
     }
   });
-  
+
   // 控制台风标记显示/隐藏
   if (tbentity.value) {
     tbentity.value.show = typhoonVisible.value;
   }
-  
+
+  // 如果台风隐藏,同时隐藏信息弹窗
+  if (!typhoonVisible.value) {
+    typhoonInfoVisible.value = false;
+  }
+
   // 如果显示台风,则跳转到台风视角
   if (typhoonVisible.value) {
     viewer.camera.flyTo({
@@ -174,7 +204,7 @@ onMounted(async () => {
   document.body.style.height = '100%';
   document.body.style.margin = '0';
   document.body.style.padding = '0';
-  
+
   // 初始化Cesium viewer
   viewer = new Cesium.Viewer('cesiumContainer', {
     timeline: false,
@@ -245,7 +275,7 @@ onMounted(async () => {
   JYLData.forEach((item) => {
     const position = Cesium.Cartesian3.fromDegrees(
       parseFloat(item.LGTD),
-      parseFloat(item.LTTD) 
+      parseFloat(item.LTTD)
     );
     const entity = viewer.entities.add({
       position: position,
@@ -326,6 +356,9 @@ onMounted(async () => {
     const correctedY = click.position.y / scaleRatio;
     const pickedObject = viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
 
+    // 隐藏台风信息弹窗
+    typhoonInfoVisible.value = false;
+
     if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
       const entityId = pickedObject.id.id;
       const data = entityDataMap.get(entityId) || pickedObject.id.properties?.data?.getValue();
@@ -349,9 +382,50 @@ onMounted(async () => {
     }
   }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
 
+  // 添加鼠标移动事件处理 - 用于台风路径点悬停
+  handler.setInputAction((movement) => {
+    // 只有台风可见时才处理台风信息弹窗
+    if (!typhoonVisible.value) {
+      typhoonInfoVisible.value = false;
+      return;
+    }
+
+    const scaleRatio = getScaleRatio();
+    const correctedX = movement.endPosition.x / scaleRatio;
+    const correctedY = movement.endPosition.y / scaleRatio;
+    const pickedObject = viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
+
+    // 检查是否悬停在台风路径点上
+    if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
+      const entityId = pickedObject.id.id;
+      // 检查是否是台风路径点
+      if (entityId && entityId.startsWith('typhoon-point-')) {
+        const typhoonData = typhoonPointDataMap.value.get(entityId);
+        if (typhoonData) {
+          // 显示台风信息弹窗
+          typhoonInfo.value = typhoonData;
+          typhoonInfoVisible.value = true;
+
+          // 计算弹窗位置
+          const entityPosition = viewer.scene.cartesianToCanvasCoordinates(pickedObject.id.position._value);
+          if (entityPosition) {
+            typhoonPopupPosition.value = {
+              x: (entityPosition.x / scaleRatio) - 120,
+              y: (entityPosition.y / scaleRatio) - 200
+            };
+          }
+          return; // 找到台风点后不再处理其他逻辑
+        }
+      }
+    }
+
+    // 如果不是台风路径点,隐藏台风信息弹窗
+    typhoonInfoVisible.value = false;
+  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+
   // 初始化台风相关功能
   initTyphoonVisualization();
-  
+
   // 监听窗口大小变化,确保地图铺满
   window.addEventListener('resize', handleResize);
   // 初始触发一次 resize 确保地图正确显示
@@ -362,14 +436,14 @@ onMounted(async () => {
 const initTyphoonVisualization = () => {
   // 设置Cesium Ion访问令牌
   Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ODIwOGQ2Ny1hMTFhLTQ4OGQtODJhZi0wNmMzZGNhNjU5OWMiLCJpZCI6NTkzMTMsImlhdCI6MTYyMzk4ODQ4NX0.40CU0i0LswshdxVXAXEJgfEDJN3EK_jPbo_S8lece9E';
-  
+
   // 创建台风数据源
   myEntityCollection.value = new Cesium.CustomDataSource("typhoonPoints");
   viewer.dataSources.add(myEntityCollection.value);
-  
+
   // 初始化警戒线
   initJJ();
-  
+
   // 加载台风数据
   initPoints();
 };
@@ -378,53 +452,77 @@ const initTyphoonVisualization = () => {
 const initPoints = () => {
   // 加载台风JSON数据
   const jsonUrl = new URL('../assets/Data/202508.json', import.meta.url).href;
-  
+
   axios.get(jsonUrl)
     .then(response => {
-      const points = response.data.points;
-      processPoints(points);
+      const typhoonData = response.data;
+      // 处理台风数据,使用response.data.points
+      processPoints(typhoonData.points, typhoonData);
     })
     .catch(error => {
       console.error('加载台风JSON数据失败,使用测试数据', error);
-      // 测试数据
-      const testPoints = [
-        {
-          "lng": 120,
-          "lat": 20,
-          "strong": "台风",
-          "forecast": [
-            {
-              "forecastpoints": [
-                {"lng": 121, "lat": 19},
-                {"lng": 122, "lat": 18}
-              ]
-            }
-          ]
-        },
-        {
-          "lng": 121,
-          "lat": 19.5,
-          "strong": "强台风",
-          "forecast": []
-        }
-      ];
-      processPoints(testPoints);
+      // 测试数据 - 包含详细台风信息
+      const testTyphoonData = {
+        "name": "竹节草",
+        "enname": "CO-MAY",
+        "points": [
+          {
+            "time": "2025-07-23 14:00:00",
+            "lng": "119.20",
+            "lat": "18.40",
+            "strong": "热带低压",
+            "power": "7",
+            "speed": "15",
+            "pressure": "998",
+            "movespeed": "14",
+            "movedirection": "南西",
+            "radius7": "150-280公里",
+            "radius10": "--",
+            "radius12": "--",
+            "forecast": [
+              {
+                "tm": "中国",
+                "forecastpoints": [
+                  { "lng": 121, "lat": 19 },
+                  { "lng": 122, "lat": 18 }
+                ]
+              }
+            ]
+          },
+          {
+            "time": "2025-07-23 17:00:00",
+            "lng": "119.10",
+            "lat": "18.00",
+            "strong": "热带低压",
+            "power": "7",
+            "speed": "15",
+            "pressure": "996",
+            "movespeed": "13",
+            "movedirection": "南南西",
+            "radius7": "180-300公里",
+            "radius10": "50-80公里",
+            "radius12": "--",
+            "forecast": []
+          }
+        ]
+      };
+      processPoints(testTyphoonData.points, testTyphoonData);
     });
 };
 
 // 处理台风数据
-const processPoints = (points) => {
+const processPoints = (points, typhoonData) => {
   const lineArr = [];
-  
-  points.forEach(element => {
+
+  points.forEach((element, index) => {
     // 强制转换为数字类型
     const lng = Number(element.lng);
     const lat = Number(element.lat);
-    
+
     let color = Cesium.Color.RED;
-    
+
     // 根据台风强度设置颜色
-    switch(element.strong) {
+    switch (element.strong) {
       case "热带低压":
         color = Cesium.Color.GREEN;
         break;
@@ -444,18 +542,40 @@ const processPoints = (points) => {
         color = Cesium.Color.RED;
         break;
     }
-    
+
     lineArr.push(lng, lat);
+    // 为每个台风路径点创建唯一ID
+    const pointId = `typhoon-point-${index}`;
     const entity = new Cesium.Entity({
+      id: pointId,
       position: Cesium.Cartesian3.fromDegrees(lng, lat),
       point: {
-        pixelSize: 5,
-        color: color
+        pixelSize: 8,  // 增大点的尺寸,更容易被鼠标选中
+        color: color,
+        outlineColor: Cesium.Color.WHITE,
+        outlineWidth: 2
       }
     });
     myEntityCollection.value.entities.add(entity);
+
+    // 存储台风点与数据的映射关系,使用正确的属性名
+    typhoonPointDataMap.value.set(pointId, {
+      name: typhoonData.name || '',
+      enname: typhoonData.enname || '',
+      time: element.time || '',
+      lng: element.lng,
+      lat: element.lat,
+      speed: element.speed,
+      power: element.power,
+      pressure: element.pressure,
+      movedirection: element.movedirection,
+      movespeed: element.movespeed,
+      radius7: element.radius7 || '--',
+      radius10: element.radius10 || '--',
+      radius12: element.radius12 || '--'
+    });
   });
-  
+
   // 添加台风路径线
   const pathEntity = viewer.entities.add({
     polyline: {
@@ -466,12 +586,39 @@ const processPoints = (points) => {
     }
   });
   typhoonRelatedEntities.value.paths.push(pathEntity);
-  
+
+  // 添加台风登陆点标记
+  if (typhoonData.land && typhoonData.land.length > 0) {
+    typhoonData.land.forEach((landPoint, index) => {
+      const landEntity = viewer.entities.add({
+        position: Cesium.Cartesian3.fromDegrees(
+          Number(landPoint.lng), 
+          Number(landPoint.lat)
+        ),
+        point: {
+          pixelSize: 12,
+          color: Cesium.Color.BLACK,
+          outlineColor: Cesium.Color.YELLOW,
+          outlineWidth: 2
+        },
+        label: {
+          text: `登陆 ${index + 1}`,
+          font: '14px 微软雅黑',
+          fillColor: Cesium.Color.YELLOW,
+          backgroundColor: Cesium.Color.BLACK.withAlpha(0.7),
+          showBackground: true,
+          pixelOffset: new Cesium.Cartesian2(0, -20)
+        }
+      });
+      myEntityCollection.value.entities.add(landEntity);
+    });
+  }
+
   if (points.length > 0) {
     // 初始化预报路径
     initForeast(points[points.length - 1]);
     // 添加台风动画
-    adds(points);
+    adds(points, typhoonData);
   }
 };
 
@@ -485,37 +632,40 @@ const initForeast = (data) => {
     Cesium.Color.fromCssColorString("#E76F15"),
     Cesium.Color.fromCssColorString("#15D9E7")
   ];
-  
+
   forecast.forEach((ele, ii) => {
-    const lineArr = [];
-    ele.forecastpoints.forEach(e => {
-      // 强制转换为数字
-      const lng = Number(e.lng);
-      const lat = Number(e.lat);
-      lineArr.push(lng, lat);
-      
-      const entity = new Cesium.Entity({
-        position: Cesium.Cartesian3.fromDegrees(lng, lat),
-        point: {
-          pixelSize: 7,
-          color: colorArr[ii]
+    // 适配新的数据结构,预报点在tm对象下的forecastpoints中
+    if (ele.forecastpoints && ele.forecastpoints.length > 0) {
+      const lineArr = [];
+      ele.forecastpoints.forEach(e => {
+        // 强制转换为数字
+        const lng = Number(e.lng);
+        const lat = Number(e.lat);
+        lineArr.push(lng, lat);
+
+        const entity = new Cesium.Entity({
+          position: Cesium.Cartesian3.fromDegrees(lng, lat),
+          point: {
+            pixelSize: 7,
+            color: colorArr[ii % colorArr.length]
+          }
+        });
+        myEntityCollection.value.entities.add(entity);
+      });
+
+      // 添加预报路径线
+      const forecastEntity = viewer.entities.add({
+        polyline: {
+          positions: Cesium.Cartesian3.fromDegreesArray(lineArr),
+          width: 2,
+          clampToGround: true,
+          material: new Cesium.PolylineDashMaterialProperty({
+            color: colorArr[ii % colorArr.length]
+          })
         }
       });
-      myEntityCollection.value.entities.add(entity);
-    });
-    
-    // 添加预报路径线
-    const forecastEntity = viewer.entities.add({
-      polyline: {
-        positions: Cesium.Cartesian3.fromDegreesArray(lineArr),
-        width: 2,
-        clampToGround: true,
-        material: new Cesium.PolylineDashMaterialProperty({
-          color: colorArr[ii]
-        })
-      }
-    });
-    typhoonRelatedEntities.value.forecasts.push(forecastEntity);
+      typhoonRelatedEntities.value.forecasts.push(forecastEntity);
+    }
   });
 };
 
@@ -534,7 +684,7 @@ const initJJ = () => {
     }
   });
   typhoonRelatedEntities.value.warnings.push(line24h);
-  
+
   // 48小时警戒线
   const line48h = viewer.entities.add({
     name: '48小时警戒线',
@@ -548,7 +698,7 @@ const initJJ = () => {
     }
   });
   typhoonRelatedEntities.value.warnings.push(line48h);
-  
+
   // 警戒线标签
   const label24h = viewer.entities.add({
     position: Cesium.Cartesian3.fromDegrees(126.129019, 29.104287),
@@ -559,7 +709,7 @@ const initJJ = () => {
     }
   });
   typhoonRelatedEntities.value.warnings.push(label24h);
-  
+
   const label48h = viewer.entities.add({
     position: Cesium.Cartesian3.fromDegrees(132, 20),
     label: {
@@ -572,20 +722,23 @@ const initJJ = () => {
 };
 
 // 添加台风动画
-const adds = (data) => {
-  addTB();
-  
+const adds = (data, typhoonData) => {
+  addTB(typhoonData);
+
   // 清除可能存在的旧定时器
   if (typhoonInterval) {
     clearInterval(typhoonInterval);
   }
-  
+
   // 设置台风动画定时器
   typhoonInterval = setInterval(() => {
+    // 只有台风可见时才更新动画
+    if (!typhoonVisible.value) return;
+
     if (iii.value >= data.length) {
       iii.value = 0;
     }
-    
+
     const kkk = iii.value * 2;
     // 确保经纬度是数字类型
     const currentData = data[iii.value];
@@ -611,14 +764,14 @@ const adds = (data) => {
         radius4: 170 - kkk
       }
     };
-    
+
     if (tbentity.value) {
       tbentity.value.position = Cesium.Cartesian3.fromDegrees(
         Number(currentData.lng),
         Number(currentData.lat)
       );
     }
-    
+
     iii.value = (iii.value + 1) % data.length;
     removeTFLayer();
     // 只有当台风可见时才添加风圈
@@ -629,24 +782,28 @@ const adds = (data) => {
 };
 
 // 添加台风标记
-const addTB = () => {
+const addTB = (typhoonData) => {
   // 尝试使用GIF动画标记
   const SuperGif = window.SuperGif;
   if (typeof SuperGif !== 'function') {
     console.warn('SuperGif未加载成功,使用默认标记');
-    useDefaultMarker();
+    useDefaultMarker(typhoonData);
     return;
   }
-  
+
   const img = document.createElement('img');
   img.src = '/tf.gif'; // public目录下的GIF
-  
+
   img.onload = () => {
     try {
       const rub = new SuperGif({ gif: img });
       rub.load(() => {
         tbentity.value = viewer.entities.add({
-          position: Cesium.Cartesian3.fromDegrees(75.166493, 39.9060534),
+          // 使用台风的中心经纬度作为初始位置
+          position: Cesium.Cartesian3.fromDegrees(
+            Number(typhoonData.centerlng || 123.75), 
+            Number(typhoonData.centerlat || 28.95)
+          ),
           billboard: {
             image: new Cesium.CallbackProperty(() => {
               return rub.get_canvas().toDataURL('image/png');
@@ -657,20 +814,23 @@ const addTB = () => {
       });
     } catch (error) {
       console.error('GIF处理失败,使用默认标记', error);
-      useDefaultMarker();
+      useDefaultMarker(typhoonData);
     }
   };
-  
+
   img.onerror = () => {
     console.error('GIF加载失败,使用默认标记');
-    useDefaultMarker();
+    useDefaultMarker(typhoonData);
   };
 };
 
 // 使用默认标记
-const useDefaultMarker = () => {
+const useDefaultMarker = (typhoonData) => {
   tbentity.value = viewer.entities.add({
-    position: Cesium.Cartesian3.fromDegrees(75.166493, 39.9060534),
+    position: Cesium.Cartesian3.fromDegrees(
+      Number(typhoonData.centerlng || 123.75), 
+      Number(typhoonData.centerlat || 28.95)
+    ),
     point: {
       pixelSize: 15,
       color: Cesium.Color.RED,
@@ -691,7 +851,7 @@ const removeTFLayer = () => {
 // 添加台风风圈
 const addTyphoonCircle = () => {
   if (!currentPointObj.value || !typhoonVisible.value) return;
-  
+
   const circles = ['circle7', 'circle10', 'circle12'];
   circles.forEach(item => {
     const entity = viewer.entities.add({
@@ -739,12 +899,12 @@ const getTyphoonPolygonPoints = (pointObj, cNum) => {
   ];
   const startAngleList = [0, 90, 180, 270];
   let fx, fy;
-  
+
   startAngleList.forEach((startAngle, index) => {
     const radius = radiusList[index] / 100;
     const pointNum = 90;
     const endAngle = startAngle + 90;
-    
+
     for (let i = 0; i <= pointNum; i++) {
       const angle = startAngle + ((endAngle - startAngle) * i) / pointNum;
       const sin = Math.sin((angle * Math.PI) / 180);
@@ -752,34 +912,41 @@ const getTyphoonPolygonPoints = (pointObj, cNum) => {
       const x = center[0] + radius * sin;
       const y = center[1] + radius * cos;
       points.push(x, y);
-      
+
       if (startAngle === 0 && i === 0) {
         fx = x;
         fy = y;
       }
     }
   });
-  
+
   points.push(fx, fy);
   return points;
 };
 
+// 处理窗口大小变化
+const handleResize = () => {
+  if (viewer) {
+    viewer.resize();
+  }
+};
+
 // 清理函数
 onUnmounted(() => {
   // 清除台风动画定时器
   if (typhoonInterval) {
     clearInterval(typhoonInterval);
   }
-  
+
   // 移除窗口大小变化监听
   window.removeEventListener('resize', handleResize);
-  
+
   // 销毁事件处理器
   if (handler) {
     handler.destroy();
     handler = null;
   }
-  
+
   // 销毁viewer
   if (viewer && !viewer.isDestroyed()) {
     viewer.destroy();
@@ -788,7 +955,6 @@ onUnmounted(() => {
 </script>
 
 <style scoped>
-
 /* 控制按钮样式 */
 .control-buttons {
   position: absolute;
@@ -825,7 +991,8 @@ onUnmounted(() => {
 }
 
 
-.custom-popup {
+.custom-popup,
+.typhoon-popup {
   position: absolute;
   z-index: 1000;
   display: block;
@@ -837,7 +1004,7 @@ onUnmounted(() => {
   border-radius: 5px;
   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
   padding: 10px 15px;
-  width: 200px;
+  width: 240px;
   pointer-events: all;
 }
 
@@ -857,6 +1024,7 @@ onUnmounted(() => {
   margin-top: 0;
   margin-bottom: 8px;
   color: #333;
+  font-size: 16px;
 }
 
 .popup-content p {
@@ -877,6 +1045,7 @@ onUnmounted(() => {
   padding: 10px 15px;
   border-radius: 4px;
   margin: 0;
+  transition: opacity 0.3s ease, transform 0.3s ease;
 }
 
 .legend li {
@@ -913,3 +1082,4 @@ onUnmounted(() => {
   background: red;
 }
 </style>
+