components/SpeedLimitProfile/Map.tsx

src/ui/components/widgets/SpeedLimitSection/components/SpeedLimitProfile/Map.tsx
mail@pastecode.io avatar
unknown
typescript
16 days ago
10 kB
3
Indexable
Never
import {
  MapContainer,
  Marker,
  Polyline,
  TileLayer,
  Tooltip,
  ZoomControl,
  useMap
} from 'react-leaflet'
import L from 'leaflet'
import * as MapConfig from './config'
import { Stack, Switch, Typography, styled } from '@mui/material'
import { CCard } from '@coreui/react'
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { divIcon } from 'leaflet'
import { LoadingSpinner } from '@Shared'
import { useSpeedLimitStore } from '@Contexts'

const SpeedLimitMap = () => {
  const {
    trackCoordinatesByRouteId: {
      data: trackCoordinatesData,
      loading: trackCoordinatesLoading,
      error: trackCoordinatesError
    }
  } = useSelector((state: any) => state.portalData)

  const {
    stationsByRouteId: {
      data: stationsData,
      loading: stationsLoading,
      error: stationsError
    }
  } = useSelector((state: any) => state.portalData)

  const {
    speedLimitsDataByRouteId: {
      // data: speedLimitsData,
      loading: speedLimitsDataLoading,
      error: speedLimitsDataError
    }
  } = useSelector((state: any) => state.portalData)

  const speedLimitData = useSpeedLimitStore.use.speedLimits()

  //for toggles
  const [showMilepostMarkers, setshowMilepostMarkers] = useState(true)
  const [showMileposts, setShowMileposts] = useState(true)
  const [showSpeedLimits, setShowSpeedLimits] = useState(true)

  //UI for the toggles
  const AntSwitch = styled(Switch)(({ theme }) => ({
    width: 28,
    height: 16,
    padding: 0,
    display: 'flex',
    '&:active': {
      '& .MuiSwitch-thumb': {
        width: 15
      },
      '& .MuiSwitch-switchBase.Mui-checked': {
        transform: 'translateX(9px)'
      }
    },
    '& .MuiSwitch-switchBase': {
      padding: 2,
      '&.Mui-checked': {
        transform: 'translateX(12px)',
        color: '#fff',
        '& + .MuiSwitch-track': {
          opacity: 1,
          backgroundColor: theme.palette.mode === 'dark' ? '#177ddc' : '#1890ff'
        }
      }
    },
    '& .MuiSwitch-thumb': {
      boxShadow: '0 2px 4px 0 rgb(0 35 11 / 20%)',
      width: 12,
      height: 12,
      borderRadius: 6,
      transition: theme.transitions.create(['width'], {
        duration: 200
      })
    },
    '& .MuiSwitch-track': {
      borderRadius: 16 / 2,
      opacity: 1,
      backgroundColor:
        theme.palette.mode === 'dark'
          ? 'rgba(255,255,255,.35)'
          : 'rgba(0,0,0,.25)',
      boxSizing: 'border-box'
    }
  }))

  //for automatic adjustments to the map
  const bounds =
    stationsData?.length > 0
      ? L.latLngBounds(
          stationsData?.map((station: any) => [station.lat, station.lng])
        )
      : L.latLngBounds([
          [0, 0],
          [0, 0]
        ])

  const FitBounds = ({ bounds }: { bounds: L.LatLngBounds }) => {
    const map = useMap()
    useEffect(() => {
      if (bounds.isValid()) {
        map.fitBounds(bounds)
      } else {
        console.error('Bounds are not valid.')
      }
    }, [map, bounds])
    return null
  }

  //creating icons that will be displayed for the map
  function createMilepostIcon(milepost: number) {
    return divIcon({
      className:
        'w-full h-full rounded-full flex items-center justify-center text-black',
      iconSize: [20, 20],
      html: `<div style="background-color: white; border: 2px solid gray; border-radius: 50%; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 8px; font-weight: bold;">${milepost}</div>`
    })
  }
  function createTSOIcon(milepost: number) {
    return divIcon({
      className:
        'w-full h-full rounded-full flex items-center justify-center text-black',
      iconSize: [20, 20],
      html: `<div style="background-color: #fcb503; border: 2px solid gray; border-radius: 50%; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 8px; font-weight: bold;">${milepost}</div>`
    })
  }
  const stationMarkerIcon = divIcon({
    className: 'w-full h-full bg-[#555555] rounded-full',
    iconSize: [12, 12]
  })

  //function to render mileposts markers, every 10 Miles, only unique markers
  function renderMilepostMarkers() {
    const processedMileposts = new Set()
    return trackCoordinatesData
      ?.filter((trackCoord: any) => {
        const flooredMilepost = Math.floor(trackCoord?.milepost)
        if (
          flooredMilepost % 10 === 0 &&
          !processedMileposts.has(flooredMilepost)
        ) {
          processedMileposts.add(flooredMilepost)
          return true
        }
        return false
      })
      ?.map((trackCoord: any) => (
        <Marker
          key={trackCoord?.milepost}
          position={[
            trackCoord?.gps_coordinates?.lat,
            trackCoord?.gps_coordinates?.lng
          ]}
          icon={createMilepostIcon(Math.floor(trackCoord?.milepost))}
        ></Marker>
      ))
  }

  if (stationsLoading || trackCoordinatesLoading || speedLimitsDataLoading) {
    return (
      <div className="text-center flex items-center justify-center">
        <div>
          <LoadingSpinner classNames="my-5" />
          <p>Loading map and track data...</p>
        </div>
      </div>
    )
  }

  return (
    <div className="flex justify-center h-[480px] w-full">
      <CCard
        style={{
          width: '100%',
          height: '70vh'
        }}
      >
        <MapContainer
          center={[43.64606677, -79.37180328]}
          zoom={MapConfig.DEFAULT_ZOOM_LEVEL}
          doubleClickZoom={false}
          scrollWheelZoom={MapConfig.SCROLL_WHEEL_ZOOM}
          zoomControl={false}
        >
          <Polyline
            positions={(trackCoordinatesData || [])?.map((trackCoord: any) => [
              trackCoord?.gps_coordinates?.lat,
              trackCoord?.gps_coordinates?.lng
            ])}
            color="#57a7f2"
          />

          {showMileposts && renderMilepostMarkers()}
          <FitBounds bounds={bounds} />

          <TileLayer
            attribution={MapConfig.MAP_ATTRIBUTION}
            url={MapConfig.MAP_URLS.MAPBOX_BASIC}
          />
          <ZoomControl position="bottomright" />
          {showMilepostMarkers &&
            stationsData?.map((station: any) => (
              <Marker
                key={station?.abbreviation}
                position={[station?.lat, station?.lng]}
                icon={stationMarkerIcon}
              >
                <Tooltip direction="top" permanent>
                  <span style={{ fontSize: '0.7rem', fontWeight: 'bold' }}>
                    {station?.abbreviation}
                  </span>
                </Tooltip>
                <div className="relative h-[12px] w-[12px]"></div>
              </Marker>
            ))}
          {showSpeedLimits &&
            speedLimitData?.map((item: any) => {
              return (
                item?.start_gps_coordinates?.lat !== undefined &&
                item?.start_gps_coordinates?.lng !== undefined && (
                  <Marker
                    key={item?.temporary_speed_limit_id}
                    position={[
                      item?.start_gps_coordinates?.lat,
                      item?.start_gps_coordinates?.lng
                    ]}
                    icon={createTSOIcon(Math.floor(item?.start_mile))}
                    zIndexOffset={1000}
                  >
                    <Tooltip direction="top">
                      <div style={{ textAlign: 'center' }}>
                        <span
                          style={{ fontSize: '0.7rem', fontWeight: 'bold' }}
                        >
                          {item?.subdivision} - {item?.speed_limit_mph} MPH
                          <br />
                          Start Mile: {item?.start_mile}
                          <br />
                          End Mile: {item?.end_mile}
                        </span>
                      </div>
                    </Tooltip>
                  </Marker>
                )
              )
            })}
        </MapContainer>
        <Stack
          spacing={1}
          direction="column"
          style={{ position: 'absolute', top: 20, left: 20, zIndex: 1000 }}
        >
          <div style={{ display: 'flex', alignItems: 'center' }}>
            <AntSwitch
              checked={showMileposts}
              onChange={(event: any) => setShowMileposts(event.target.checked)}
              inputProps={{ 'aria-label': 'ant design' }}
            />
            <Typography
              style={{
                marginLeft: '10px',
                fontFamily: 'inter',
                color: 'black',
                fontWeight: 'bold',
                fontSize: '0.8rem'
              }}
            >
              Mileposts
            </Typography>
          </div>
          <div style={{ display: 'flex', alignItems: 'center' }}>
            <AntSwitch
              checked={showMilepostMarkers}
              onChange={(event: any) =>
                setshowMilepostMarkers(event.target.checked)
              }
              inputProps={{ 'aria-label': 'ant design' }}
            />
            <Typography
              style={{
                marginLeft: '10px',
                fontFamily: 'Inter',
                color: 'black',
                fontWeight: 'bold',
                fontSize: '0.8rem'
              }}
            >
              Stations
            </Typography>
          </div>
          <div style={{ display: 'flex', alignItems: 'center' }}>
            <AntSwitch
              checked={showSpeedLimits}
              onChange={(event: any) =>
                setShowSpeedLimits(event.target.checked)
              }
              inputProps={{ 'aria-label': 'ant design' }}
            />
            <Typography
              style={{
                marginLeft: '10px',
                fontFamily: 'Inter',
                color: 'black',
                fontWeight: 'bold',
                fontSize: '0.8rem'
              }}
            >
              Speed Limits
            </Typography>
          </div>
        </Stack>
      </CCard>
    </div>
  )
}

export default SpeedLimitMap
Leave a Comment