Маппим точки на карту

Предлагаемая архитектура:

Предлагаю для отображения точке на карте использовать апи запрос и стриминг точек в сокете. При первой инициализации карты (при первом отображении) отправляете апи запрос, который возвращает все точки на карте -> отображаем их. Сразу после этого начинаем отображать точки, пришедшие из сокета

Пример файла апи

api.ts:

import axios, { AxiosResponse } from "axios";

export const instance = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
});

export const api = {
  async getMapData() {
    const response = await instance.get(`/api/map_data`);
    return response.data;
  },
};

для baseURL у вас должна быть переменная окружения (если используете сборщик VITE, то все переменные окружение должны начинаться с VITE_ ) в .env файле:

Пример для локального использования

VITE_API_URL=http://localhost:5173

В результате baseURL будет иметь вид:

baseURL: http://localhost:5173//api/map_data

Пример сокета с использованием Zustand (хранилище)

socket-provider.ts:

export interface MapDataPoint {
  time: string
  lat: number
  lon: number
  data: {
    rsrp: number | null
  }
}

export const useTrialsEventsPointsStore = create<{
  trialsEventsPoints: MapDataPoint | null
  setTrialsEventsPoints: (trialsEventsPoints: MapDataPoint) => void
}>(set => ({
  trialsEventsPoints: null,
  setTrialsEventsPoints: trialsEventsPoints => set({ trialsEventsPoints })
}))
export const useTrialsEventsWebSocketStore = create<{
  trialsEventsSocket: WebSocket | null
  isTrialsEventsConnected: boolean
  connectTrialsEvents: () => void
  disconnectTrialsEvents: () => void
}>(set => ({
  trialsEventsSocket: null,
  isTrialsEventsConnected: false,
  connectTrialsEvents: () => {
    const { isTrialsEventsConnected, disconnectTrialsEvents, trialsEventsSocket } =
      useTrialsEventsWebSocketStore.getState()

    if (isTrialsEventsConnected && trialsEventsSocket?.readyState === WebSocket.OPEN) {
      console.log('Trials WebSocket already connected, skipping...')
      return
    }

    if (trialsEventsSocket) {
      disconnectTrialsEvents()
    }

    try {
      const trialsEventsSocket = new WebSocket(import.meta.env.VITE_WS_TRIALS_EVENTS)

      trialsEventsSocket.onopen = () => {
        console.log('Connected to Trials WebSocket')
        set({ isTrialsEventsConnected: true })
      }

      trialsEventsSocket.onmessage = event => {
        if (event.data === 'PONG') return

        const parsedData = JSON.parse(event.data)
        if (parsedData.name === 'map_point' && parsedData.data) { //map_point - для пример, здесь должно быть название (name) ивента из вашего сокет соединения
          useTrialsEventsPointsStore.getState().setTrialsEventsPoints(parsedData.data)
        }
      }

      trialsEventsSocket.onclose = () => {
        console.log('Disconnected from Trials WebSocket')
        set({ isTrialsEventsConnected: false })
      }

      trialsEventsSocket.onerror = error => {
        console.error('Trials WebSocket error:', error)
      }

      set({ trialsEventsSocket })
    } catch (error) {
      console.error('Failed to create Trials WebSocket connection:', error)
    }
  },
  disconnectTrialsEvents: () => {
    const { trialsEventsSocket } = useTrialsEventsWebSocketStore.getState()

    if (trialsEventsSocket) {
      trialsEventsSocket.close()
    }

    set({
      trialsEventsSocket: null,
      isTrialsEventsConnected: false
    })
  }
}))

Пример файла реализации карты

Так как нам необходимо отобразить точки, то выберем для этого соответствующий метод среди тех, которые нам предлагает Deck.gl. По названию не трудно определить, что это будет - ScatterplotLayer

https://deck.gl/examples/scatterplot-layer

Пример из документации: alt text

map.tsx:

import { MapViewState } from '@deck.gl/core'
import { ScatterplotLayer } from '@deck.gl/layers'
import { DeckGL } from '@deck.gl/react'
import { Map as MapLibreMap, NavigationControl } from 'react-map-gl/maplibre'
import { useEffect, useState } from 'react'

import * as styles from './map.style'
import { api } from '~/shared/api/api'
import { useTrialsEventsWebSocketStore } from '~/shared/sockets/socket-provider'
import { useTrialsEventsPointsStore } from '~/shared/sockets/socket-provider'

export interface MapDataPoint {
  time: string
  lat: number
  lon: number
  data: {
    rsrp: number | null
  }
}

const INITIAL_VIEW_STATE: MapViewState = { // здесь инициализируем начальное положение карты, как вариант задаем все статикой, но можем спокойно брать лат,лон первой точки
  longitude: 82.9346,
  latitude: 55.0415,
  zoom: 10
}

export const Map = () => {
  const [mapData, setMapData] = useState<MapDataPoint[]>([])
  const [layers, setLayers] = useState<any[]>([])

  const { connectTrialsEvents, disconnectTrialsEvents } = useTrialsEventsWebSocketStore()
  const { trialsEventsPoints } = useTrialsEventsPointsStore()

  useEffect(() => {
    const fetchMapData = async () => {
      try {
        const data = await api.getMapData() // получаем первоначальные данные
        setMapData(data)

        // Открываем сокет соединение только после успешного получения данных
        connectTrialsEvents()
      } catch (error) {
        console.error('Ошибка загрузки данных карты:', error)
      }
    }

    fetchMapData()

    return () => {
      disconnectTrialsEvents()
    }
  }, [connectTrialsEvents, disconnectTrialsEvents])

  useEffect(() => {
    if (trialsEventsPoints !== null) {
      setMapData(prevData => [...prevData, trialsEventsPoints])
    }
  }, [trialsEventsPoints])

  useEffect(() => {
  if (mapData.length === 0) return

  const scatterplotLayer = new ScatterplotLayer<MapDataPoint>({
    id: 'rsrp-layer',
    data: mapData,
    pickable: true,
    opacity: 0.8,
    stroked: false,
    filled: true,
    radiusScale: 6,
    radiusMinPixels: 4,
    radiusMaxPixels: 4,
    lineWidthMinPixels: 1,
    getPosition: (d: MapDataPoint) => [d.lon, d.lat],
    getFillColor: (d: MapDataPoint) => {
      const rsrp = d.data.nof_pdsch

      if (rsrp < -112) {
        return [255, 0, 0, 255] // красный
      } else if (rsrp >= -112 && rsrp < -100) {
        return [255, 165, 0, 255] // оранжевый
      } else if (rsrp >= -100 && rsrp < -85) {
        return [255, 255, 0, 255] // желтый
      } else {
        return [0, 255, 0, 255] // зеленый
      }
    },
    getRadius: 5
  })

  setLayers([scatterplotLayer])
}, [mapData])
      // базовый стиль тайлов карты mapStyle (может не сработать), лучше воспользоваться специализированными сервисами, который предосталвяют тайлы например MapTiler
  return (
    <div className={styles.mapContainer}>
      <DeckGL
        initialViewState={INITIAL_VIEW_STATE}
        controller
        layers={layers}
      >
        <MapLibreMap
          mapStyle="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
        >
          <NavigationControl position='top-right' />
        </MapLibreMap>
      </DeckGL>
    </div>
  )
}

Как примерно должно выглядеть

alt text

Ссылка на документацию Deck.gl:

https://deck.gl/docs