效果演示

演示地址:https://astrodata.tech

效果截图:

2024-09-19-11.33.38.webp

核心实现

本文将介绍如何使用 MapLibre GLReact 构建一个交互式的地球地图应用,实现以下功能:

  • 定制地图样式:使用自定义的地图样式,打造独特的视觉效果。
  • 图例与交互:在地图上显示数据中心和边缘节点的标记,并实现悬停提示信息。
  • 边缘过渡遮罩:在地图的边缘添加过渡遮罩,增强视觉体验。

定制地图样式

为了使地图具有独特的风格,我使用了自定义的地图样式文件 map_style.json。这个样式文件可以通过 Mapbox Studio 或其他工具生成。

步骤:

  1. 引入自定义样式文件:

    import localMapStyle from "@/assets/map_style.json";
  2. Map 组件中使用自定义样式:

  3. 配置初始视图状态:

    const initialViewState = {
     latitude: 0,
     longitude: 0,
     zoom: 1,
    };

通过以上步骤,地图将使用我们自定义的样式进行渲染。

图例与交互

为了在地图上显示数据中心和边缘节点的信息,我们需要:

  • 加载数据:从 JSON 文件中加载数据中心和边缘节点的位置和信息。
  • 渲染标记:使用 Marker 组件在地图上标记这些位置。
  • 添加交互:在用户悬停或点击标记时显示详细信息。

1. 加载数据:

import dataCenters from "@/assets/data_centers_format.json";
import boundNodeLocations from "@/assets/boundary_node_locations.json";

2. 渲染数据中心标记:

{dataCenters.data_centers.map((center) => (
  <Marker
    key={center.key}
    latitude={center.latitude}
    longitude={center.longitude}
    anchor="center"
  >
    <div
      className="data-center-marker"
      onMouseEnter={() => handleMouseEnter(center)}
      onMouseLeave={handleMouseLeave}
    />
  </Marker>
))}

3. 渲染边缘节点标记:

{boundNodeLocations.locations.map((node) => (
  <Marker
    key={node.key}
    latitude={node.latitude}
    longitude={node.longitude}
    anchor="center"
  >
    <div
      className="edge-node-marker"
      onMouseEnter={() => handleMouseEnter(node)}
      onMouseLeave={handleMouseLeave}
    >
      <div className="edge-node-ping"></div>
    </div>
  </Marker>
))}

4. 实现交互效果:

const [hoverInfo, setHoverInfo] = useState(null);

const handleMouseEnter = (info) => {
  setHoverInfo({
    ...info,
    longitude: info.longitude,
    latitude: info.latitude,
  });
};

const handleMouseLeave = () => {
  setHoverInfo(null);
};

5. 显示悬停信息 (Popup):

{hoverInfo && (
  <Popup
    longitude={hoverInfo.longitude}
    latitude={hoverInfo.latitude}
    anchor="bottom"
    closeButton={false}
    closeOnClick={false}
  >
    <div className="popup-content">
      <div className="popup-title">{hoverInfo.name}</div>
      {/* 根据类型显示不同的信息 */}
      {hoverInfo.type === "dataCenter" ? (
        <div className="popup-info">
          <div>数据中心</div>
          <div>拥有者:{hoverInfo.owner}</div>
          <div>活跃节点:{hoverInfo.total_nodes}</div>
          <div>节点提供商:{hoverInfo.node_providers}</div>
        </div>
      ) : (
        <div className="popup-info">
          <div>边界节点</div>
          <div>节点数量:{hoverInfo.total_nodes}</div>
        </div>
      )}
    </div>
  </Popup>
)}

6. 添加样式:

在 CSS 文件中添加对应的样式,以美化标记和弹出框。

.data-center-marker {
  width: 12px;
  height: 12px;
  background-color: rgba(150, 120, 81, 0.7);
  border: 2px solid #f0a03b;
  border-radius: 50%;
  cursor: pointer;
}

.edge-node-marker {
  position: relative;
  width: 12px;
  height: 12px;
  background-color: rgba(0, 122, 255, 0.25);
  border: 2px solid #007aff;
  border-radius: 50%;
  cursor: pointer;
}

.edge-node-ping {
  position: absolute;
  top: -1px;
  left: -1px;
  width: 12px;
  height: 12px;
  background-color: rgba(0, 122, 255, 0.5);
  border-radius: 50%;
  animation: ping 2s infinite;
}

@keyframes ping {
  0% {
    transform: scale(1);
    opacity: 0.75;
  }
  100% {
    transform: scale(2);
    opacity: 0;
  }
}

.popup-content {
  font-family: Arial, sans-serif;
}

.popup-title {
  font-weight: bold;
  margin-bottom: 8px;
}

.popup-info div {
  margin-bottom: 4px;
}

边缘过渡遮罩

为了使地图的边缘具有过渡效果,我们可以添加一个覆盖层,并使用 CSS 渐变来实现。

1. 添加遮罩层:

在地图组件的底部添加一个 div

<div className="map-overlay"></div>

2. 定义遮罩层样式:

.map-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none; /* 确保遮罩层不影响地图交互 */
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 30%, #020104 70%);
}

通过上述样式,地图的边缘将呈现出由透明到半透明的渐变效果,提升视觉层次感。

相关源码

下面是完整的 EarthMap.tsx 代码:

import React, { useState } from "react";
import Map, { Marker, Popup } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
import localMapStyle from "@/assets/map_style.json";
import dataCenters from "@/assets/data_centers_format.json";
import boundNodeLocations from "@/assets/boundary_node_locations.json";

export const EarthMap = ({ width = '100vw', height = '100vh' }) => {
  const initialViewState = {
    latitude: 0,
    longitude: 0,
    zoom: 1,
  };

  const [hoverInfo, setHoverInfo] = useState(null);

  const handleMouseEnter = (info) => {
    setHoverInfo({
      ...info,
      longitude: info.longitude,
      latitude: info.latitude,
    });
  };

  const handleMouseLeave = () => {
    setHoverInfo(null);
  };

  return (
    <div style={{ height, width, position: "relative" }}>
      <Map
        initialViewState={initialViewState}
        style={{ width, height}}
        mapStyle={localMapStyle}
        mapLib={import("maplibre-gl")}
        attributionControl={true}
        scrollZoom={false}
        key={`${width}-${height}`}
      >
        {/* 渲染数据中心标记 */}
        {dataCenters.data_centers.map((center) => (
          <Marker
            key={center.key}
            latitude={center.latitude}
            longitude={center.longitude}
            anchor="center"
          >
            <div
              className="data-center-marker"
              onMouseEnter={() => handleMouseEnter({ ...center, type: "dataCenter" })}
              onMouseLeave={handleMouseLeave}
            />
          </Marker>
        ))}

        {/* 渲染边缘节点标记 */}
        {boundNodeLocations.locations.map((node) => (
          <Marker
            key={node.key}
            latitude={node.latitude}
            longitude={node.longitude}
            anchor="center"
          >
            <div
              className="edge-node-marker"
              onMouseEnter={() => handleMouseEnter({ ...node, type: "edgeNode" })}
              onMouseLeave={handleMouseLeave}
            >
              <div className="edge-node-ping"></div>
            </div>
          </Marker>
        ))}

        {/* 显示悬停信息 */}
        {hoverInfo && (
          <Popup
            longitude={hoverInfo.longitude}
            latitude={hoverInfo.latitude}
            anchor="bottom"
            closeButton={false}
            closeOnClick={false}
          >
            <div className="popup-content">
              <div className="popup-title">{hoverInfo.name}</div>
              {hoverInfo.type === "dataCenter" ? (
                <div className="popup-info">
                  <div>数据中心</div>
                  <div>拥有者:{hoverInfo.owner}</div>
                  <div>活跃节点:{hoverInfo.total_nodes}</div>
                  <div>节点提供商:{hoverInfo.node_providers}</div>
                </div>
              ) : (
                <div className="popup-info">
                  <div>边界节点</div>
                  <div>节点数量:{hoverInfo.total_nodes}</div>
                </div>
              )}
            </div>
          </Popup>
        )}
      </Map>

      {/* 边缘过渡遮罩 */}
      <div className="map-overlay"></div>
    </div>
  );
};

export default EarthMap;
  • map_style.json样式文件暂未公开;
  • data_centers_format.json等图例数据暂未公开;

相关链接


A Student on the way to full stack of Web3.