import React , { useEffect , useRef , ReactNode , useMemo } from 'react' ; import { Labels , PanelProps } from '@grafana/data' ; import { Position , TrackMapOptions , AntData } from 'types' ; import { css , cx } from 'emotion' ; import { Feature , FeatureCollection } from 'geojson' ; import { Map , Marker , Popup , TileLayer , Tooltip , withLeaflet } from 'react-leaflet' ; import { DivIcon , Icon , LatLngBounds , LatLngBoundsExpression , LeafletEvent , PointExpression } from 'leaflet' ; import './leaflet.css' ; import 'leaflet/dist/leaflet.css' ; import styled from 'styled-components' ; import { getLocationSrv } from '@grafana/runtime' ; import { stylesFactory } from '@grafana/ui' ; import ReactHtmlParser from 'react-html-parser' ; const AntPath = require ( 'react-leaflet-ant-path' ) . default ; const HeatmapLayer = require ( 'react-leaflet-heatmap-layer' ) . default ; const HexbinLayer = require ( 'react-leaflet-d3' ) . HexbinLayer ; const StyledPopup = styled ( Popup ) `
  .leaflet-popup-content-wrapper {
    white-space: pre-wrap;
  }

  .leaflet-popup-tip-container {
    visibility: hidden;
  }
` ; export const TrackMapPanel = ( { options , data , width , height } ) => { const styles = getStyles ( ) ; const mapRef = useRef < Map | null > ( null ) ; const WrappedHexbinLayer = withLeaflet ( HexbinLayer ) ; const primaryIcon = require ( 'img/marker.png' ) ; const secondaryIcon = require ( 'img/marker_secondary.png' ) ; useEffect ( ( ) => { if ( mapRef . current !== null ) { if ( options . map . zoomToDataBounds ) { const bounds = getBoundsFromPositions ( positions ) ; mapRef . current . leafletElement . fitBounds ( bounds , { animate : false } ) ; } const bounds = mapRef . current . leafletElement . getBounds ( ) ; updateMap ( bounds ) ; } } , [ ] ) ; const isLatitudeName = ( name ) : boolean => { const customLatitudeName = options . coordinates . customLatitudeColumnName !== '' ? options . coordinates . customLatitudeColumnName : '' ; return name !== '' && ( name === 'latitude' || name === 'lat' || name === customLatitudeName ) ; } ; const isLongitudeName = ( name ) : boolean => { const customLongitudeName = options . coordinates . customLongitudeColumnName !== '' ? options . coordinates . customLongitudeColumnName : '' ; return name !== '' && ( name === 'longitude' || name === 'lon' || name === customLongitudeName ) ; } ; const isPopupName = ( name ) : boolean => { return name === 'popup' || name === 'text' || name === 'desc' ; } ; const isTooltipName = ( name ) : boolean => { return name === 'tooltip' ; } ; const getAntPathColorOverridesMemoized = ( ) : ( ( ) => { [ key ] : string ; } ) => { let antPathColorOverrides = { } ; return ( ) => { if ( Object . keys ( antPathColorOverrides ) . length === 0 ) { if ( options . ant . colorOverridesByLabel ?. length ) { options . ant . colorOverridesByLabel . forEach ( ( labelColor ) => { antPathColorOverrides [ labelColor . label ] = labelColor . color ; } ) ; } } return antPathColorOverrides ; } ; } ; const getMarkerHtmlOverridesMemoized = ( ) : ( ( ) => { [ key ] : string ; } ) => { let markerHtmlOverrides = { } ; return ( ) => { if ( Object . keys ( markerHtmlOverrides ) . length === 0 ) { if ( options . marker . markerHtmlByLabel ?. length ) { options . marker . markerHtmlByLabel . forEach ( ( keyVal ) => { markerHtmlOverrides [ keyVal . key ] = keyVal . value ; } ) ; } } return markerHtmlOverrides ; } ; } ; const getAntPathColorOverrides = getAntPathColorOverridesMemoized ( ) ; const getMarkerHtmlOverrides = getMarkerHtmlOverridesMemoized ( ) ; let latitudes = data . series . map ( ( s ) => s . fields . find ( ( f ) => isLatitudeName ( f . name ) ) ?. values . toArray ( ) as number [ ] ) ; if ( ! latitudes ?. some ( ( l ) => l !== undefined ) ) { latitudes = data . series . filter ( ( f ) => isLatitudeName ( f . name ) ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Value' ) ?. values ?. toArray ( ) as number [ ] ) ; } let longitudes = data . series . map ( ( s ) => s . fields . find ( ( f ) => isLongitudeName ( f . name ) ) ?. values . toArray ( ) as number [ ] ) ; if ( ! longitudes ?. some ( ( l ) => l !== undefined ) ) { longitudes = data . series . filter ( ( f ) => isLongitudeName ( f . name ) ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Value' ) ?. values ?. toArray ( ) as number [ ] ) ; } const timestamps = data . series . filter ( ( f ) => isLatitudeName ( f . name ) ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Time' ) ?. values ?. toArray ( ) as number [ ] ) ; const labels = data . series . filter ( ( f ) => isLatitudeName ( f . name ) ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Value' ) ?. labels ) ; let intensities = data . series . map ( ( s ) => s . fields . find ( ( f ) => f . name === 'intensity' ) ?. values . toArray ( ) as number [ ] ) ; if ( ! intensities ?. some ( ( l ) => l !== undefined ) ) { intensities = data . series . filter ( ( f ) => f . name === 'intensity' ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Value' ) ?. values ?. toArray ( ) as number [ ] ) ; } let markerPopups = data . series . map ( ( s ) => s . fields . find ( ( f ) => isPopupName ( f . name ) ) ?. values . toArray ( ) as string [ ] ) ; if ( ! markerPopups ?. some ( ( l ) => l !== undefined ) ) { markerPopups = data . series . filter ( ( f ) => isPopupName ( f . name ) ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Value' ) ?. values ?. toArray ( ) as string [ ] ) ; } let markerTooltips = data . series . map ( ( s ) => s . fields . find ( ( f ) => isTooltipName ( f . name ) ) ?. values . toArray ( ) as string [ ] ) ; if ( ! markerTooltips ?. some ( ( l ) => l !== undefined ) ) { markerTooltips = data . series . filter ( ( f ) => isTooltipName ( f . name ) ) ?. map ( ( f1 ) => f1 . fields . find ( ( f ) => f . name === 'Value' ) ?. values ?. toArray ( ) as string [ ] ) ; } let iconHtml ; if ( labels && labels . length ) { iconHtml = labels . map ( ( l ) => { const overrides = getMarkerHtmlOverrides ( ) ; if ( l && l [ options . marker . labelName ] && overrides [ l [ options . marker . labelName ] ] ) { return overrides [ l [ options . marker . labelName ] ] ; } return undefined ; } ) ; } const filterZeroOrNull = ( positionSeries ) => { return positionSeries . filter ( ( pos ) => { const isNotExactlyZero = pos . longitude !== 0 || pos . latitude !== 0 ; const isNotNullOrUndefined = pos . longitude !== null && pos . latitude !== null && pos . longitude !== undefined && pos . latitude !== undefined ; return ! options . discardZeroOrNull || ( options . discardZeroOrNull && isNotExactlyZero && isNotNullOrUndefined ) ; } ) ; } ; let positions = latitudes ?. map ( ( lats , index1 ) => { return lats . map ( ( latitude , index2 ) => { const longitude = longitudes !== undefined && longitudes . length && longitudes [ index1 ] !== undefined ? longitudes [ index1 ] [ index2 ] : 0 ; const timestamp = timestamps !== undefined && timestamps . length && timestamps [ index1 ] !== undefined ? timestamps [ index1 ] [ index2 ] : 0 ; const timestampPrint = timestamp !== 0 ? `<br/>Timestamp: ${ timestamp }` : '' ; const trackLabels = labels && labels [ index1 ] ? labels [ index1 ] : undefined ; const trackLabelsPrint = trackLabels !== undefined ? `<br/>Labels: ${ JSON . stringify ( trackLabels , null , 2 ) }` : '' ; const popup = markerPopups !== undefined && markerPopups . length && markerPopups [ index1 ] !== undefined ? markerPopups [ index1 ] [ index2 ] : `${ latitude },${ longitude }${ timestampPrint }${ trackLabelsPrint }` ; const tooltip = markerTooltips !== undefined && markerTooltips . length && markerTooltips [ index1 ] !== undefined ? markerTooltips [ index1 ] [ index2 ] : undefined ; return { latitude , longitude , popup , tooltip , labels : trackLabels , } ; } ) ; } ) . map ( ( pos ) => filterZeroOrNull ( pos ) ) ; if ( ! positions || positions . length === 0 ) { positions = [ [ { latitude : 0 , longitude : 0 } ] ] ; } const heatData = [ ] ; const antData = [ ] ; const hexData = { type : 'FeatureCollection' , features : [ ] , } ; positions ?. forEach ( ( positionSeries , i ) => { const antDatas = [ ] ; const antOptions = { delay : options . ant . delay , dashArray : [ 10 , 20 ] , weight : options . ant . weight , color : options . ant . color , pulseColor : options . ant . pulseColor , paused : options . ant . paused , reverse : options . ant . reverse , } ; const currentLabels = labels [ i ] ; if ( options . ant . labelName && currentLabels && currentLabels [ options . ant . labelName ] ) { const override = getAntPathColorOverrides ( ) [ currentLabels [ options . ant . labelName ] ] ; if ( override ) { antOptions . color = override ; } } const heatDatas = [ ] ; positionSeries . forEach ( ( position , j ) => { if ( position . latitude && position . longitude ) { heatDatas . push ( [ position . latitude , position . longitude , intensities !== undefined && intensities [ i ] && intensities [ i ] [ j ] ? intensities [ i ] [ j ] : 0 , ] ) ; antDatas . push ( [ position . latitude , position . longitude ] ) ; hexData . features . push ( { type : 'Feature' , id : i , geometry : { type : 'Point' , coordinates : [ position . longitude , position . latitude ] , } , } as Feature ) ; } } ) ; heatData . push ( heatDatas ) ; antData . push ( { options : antOptions , data : antDatas , } ) ; } ) ; const createDivIcon = ( html ) => { if ( options . marker . customIconHeight !== undefined && options . marker . customIconWidth !== undefined ) { return new DivIcon ( { html , iconSize : [ options . marker . customIconWidth , options . marker . customIconHeight ] } ) ; } return new DivIcon ( { html } ) ; } ; const getOffset = ( option , defaultValue ) : PointExpression => { return option === undefined || option . trim ( ) === '' ? defaultValue : [ parseInt ( option . split ( ',' ) [ 0 ] , 10 ) , parseInt ( option . split ( ',' ) [ 1 ] , 10 ) ] ; } ; const createIcon = ( url , size ) => { return new Icon ( { iconUrl : url , iconSize : [ size , size ] , iconAnchor : getOffset ( options . marker . iconOffset , [ size * 0.5 , size ] ) , popupAnchor : getOffset ( options . marker . popupOffset , [ 0 , - size ] ) , } ) ; } ; const createMarkers = ( ) : ReactNode [ ] => { let markers = [ ] ; if ( positions && positions . length > 0 ) { positions . forEach ( ( positionSeries , i ) => { positionSeries . forEach ( ( position , j ) => { const isLastPosition = j + 1 === positionSeries . length ; const useSecondaryIcon = options . marker . useSecondaryIconForAllMarkers || ( options . marker . useSecondaryIconForLastMarker && isLastPosition ) ; let html = options . marker . defaultHtml ; if ( iconHtml && iconHtml . length ) { const maybeHtml = iconHtml [ i ] ; if ( maybeHtml !== undefined ) { html = maybeHtml ; } } let icon = createDivIcon ( html ) ; if ( ! options . marker . useHTMLForMarkers ) { icon = createIcon ( useSecondaryIcon ? secondaryIcon : primaryIcon , isLastPosition ? options . marker . sizeLast : options . marker . size ) ; } let shouldHide = options . marker . showOnlyLastMarker && ! isLastPosition ; if ( ! shouldHide ) { markers . push ( < Marker key = { i + '-' + j } position = { [ position . latitude , position . longitude ] } icon = { icon } title = { position . popup } > < StyledPopup > { ReactHtmlParser ( position . popup || '' ) } < /StyledPopup> { position . tooltip && ( < Tooltip offset = { getOffset ( options . marker . tooltipOffset , [ 0 , 0 ] ) } permanent = { options . marker . alwaysShowTooltips } > { position . tooltip } < /Tooltip>)} < /Marker>); } } ) ; } ) ; } return markers ; } ; const hexbinOptions = { colorScaleExtent : [ 1 , undefined ] , radiusScaleExtent : [ 1 , undefined ] , colorRange : [ options . hex . colorRangeFrom , options . hex . colorRangeTo ] , radiusRange : [ options . hex . radiusRangeFrom , options . hex . radiusRangeTo ] , } ; const onMapMoveEnd = ( event ) => { if ( mapRef . current !== null ) { mapRef . current . leafletElement . invalidateSize ( ) ; } updateMap ( event . target . getBounds ( ) ) ; } ; const updateQueryVariables = ( minLat , minLon , maxLat , maxLon ) => { getLocationSrv ( ) . update ( { query : { 'var-minLat' : minLat , 'var-maxLat' : maxLat , 'var-minLon' : minLon , 'var-maxLon' : maxLon , } , partial : true , replace : true , } ) ; } ; const updateMap = ( bounds ) => { const minLat = bounds . getSouthWest ( ) . lat ; const minLon = bounds . getSouthWest ( ) . lng ; const maxLat = bounds . getNorthEast ( ) . lat ; const maxLon = bounds . getNorthEast ( ) . lng ; if ( options . map . useBoundsInQuery ) { updateQueryVariables ( minLat , minLon , maxLat , maxLon ) ; } } ; const getBoundsFromPositions = ( positions ) : LatLngBoundsExpression => { if ( positions ) { const minLon = Math . min ( ... positions ?. map ( ( ps ) => ps . map ( ( p ) => p . longitude ) . flat ( ) ) . flat ( ) ) ; const maxLon = Math . max ( ... positions ?. map ( ( ps ) => ps . map ( ( p ) => p . longitude ) . flat ( ) ) . flat ( ) ) ; const minLat = Math . min ( ... positions ?. map ( ( ps ) => ps . map ( ( p ) => p . latitude ) . flat ( ) ) . flat ( ) ) ; const maxLat = Math . max ( ... positions ?. map ( ( ps ) => ps . map ( ( p ) => p . latitude ) . flat ( ) ) . flat ( ) ) ; return [ [ minLat , minLon ] , [ maxLat , maxLon ] , ] ; } else { return [ [ - 180 , 180 ] , [ - 180 , 180 ] , ] ; } } ; const mapCenter = { latitude : options . map . centerLatitude , longitude : options . map . centerLongitude , } ; if ( options . map . useCenterFromFirstPos && positions ?. length && positions [ 0 ] ?. length && positions [ 0 ] [ 0 ] . latitude ) { mapCenter . latitude = positions [ 0 ] [ 0 ] . latitude ; mapCenter . longitude = positions [ 0 ] [ 0 ] . longitude ; } if ( positions ?. length && positions [ 0 ] ?. length && positions [ 0 ] [ 0 ] ) { if ( options . map . useCenterFromFirstPos && positions [ 0 ] [ 0 ] . latitude ) { mapCenter . latitude = positions [ 0 ] [ 0 ] . latitude ; mapCenter . longitude = positions [ 0 ] [ 0 ] . longitude ; } if ( ! options . map . useCenterFromFirstPos && options . map . useCenterFromLastPos && positions [ 0 ] [ positions [ 0 ] . length - 1 ] . latitude ) { mapCenter . latitude = positions [ 0 ] [ positions . length - 1 ] . latitude ; mapCenter . longitude = positions [ 0 ] [ positions . length - 1 ] . longitude ; } } let antPaths = null ; if ( options . viewType === 'ant' || options . viewType === 'ant-marker' ) { antPaths = antData . map ( ( d , i ) => { if ( d . data . length && d . data . length > 1 ) { const popup = positions ? positions [ i ] . find ( ( p ) => p . latitude && p . longitude ) ?. popup : undefined ; return ( < AntPath key = { i } positions = { d . data } options = { d . options } > { popup ? < StyledPopup > { popup } < /StyledPopup> : null} < /AntPath>); } return null ; } ) ; } const createHeatMaps = useMemo ( ( ) : ReactNode [ ] => { return heatData . map ( ( h , i ) => ( < HeatmapLayer key = { i } fitBoundsOnLoad = { options . heat . fitBoundsOnLoad } fitBoundsOnUpdate = { options . heat . fitBoundsOnUpdate } points = { h } longitudeExtractor = { ( m ) => m [ 1 ] } latitudeExtractor = { ( m ) => m [ 0 ] } intensityExtractor = { ( m ) => parseFloat ( m [ 2 ] ) } />)); } , [ heatData ] ) ; return ( < div className = { cx ( styles . wrapper , css `
          width: ${ width }px;
          height: ${ height }px;
        ` ) } > < Map ref = { mapRef } center = { [ mapCenter . latitude , mapCenter . longitude ] } zoom = { options . map . zoom } zoomSnap = { 0.5 } onmoveend = { ( event ) => { onMapMoveEnd ( event ) ; } } > { antPaths } { options . viewType === 'heat' && createHeatMaps } { options . viewType === 'hex' && < WrappedHexbinLayer { ... hexbinOptions } data = { hexData } / > } { ( options . viewType === 'marker' || options . viewType === 'ant-marker' ) && createMarkers ( ) } < TileLayer attribution = { options . map . tileAttribution } url = { options . map . tileUrlSchema } / > < /Map> < /div>); } ; const getStyles = stylesFactory ( ( ) => { return { wrapper : css `
      position: relative;
    ` , } ; } ) ;