summaryrefslogblamecommitdiff
path: root/node_modules/pigeon-maps/src/overlays/GeoJson.tsx
blob: d25cfe6dcf4773deb9f50fefe4d6660feecc9b1e (plain) (tree)



















































































































































































































































                                                                                                                        
import React, { CSSProperties, SVGProps, useMemo, useEffect, useState } from 'react'
import { PigeonProps, Point } from '../types'

interface GeoJsonProps extends PigeonProps {
  className?: string
  data?: any
  svgAttributes?: any
  styleCallback?: any
  hover?: any
  feature?: any
  style?: CSSProperties
  children?: React.ReactNode

  // callbacks
  onClick?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
  onContextMenu?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
  onMouseOver?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
  onMouseOut?: ({ event: HTMLMouseEvent, anchor: Point, payload: any }) => void
}

interface GeoJsonLoaderProps extends GeoJsonProps {
  link?: string
}

interface GeoJsonGeometry {
  type: string
  coordinates?:
    | [number, number]
    | Array<[number, number]>
    | Array<Array<[number, number]>>
    | Array<Array<Array<[number, number]>>>
  geometries?: Array<GeoJsonGeometry>
}

interface GeometryProps {
  coordinates?:
    | [number, number]
    | Array<[number, number]>
    | Array<Array<[number, number]>>
    | Array<Array<Array<[number, number]>>>
  latLngToPixel?: (latLng: Point, center?: Point, zoom?: number) => Point
  svgAttributes?: SVGProps<SVGElement>
  geometry?: GeoJsonGeometry
}

const defaultSvgAttributes = { fill: '#93c0d099', strokeWidth: '2', stroke: 'white', r: '30' }

export function PointComponent(props: GeometryProps): JSX.Element {
  const { latLngToPixel } = props
  const [y, x] = props.coordinates as [number, number]
  const [cx, cy] = latLngToPixel([x, y])
  if (props.svgAttributes?.path) {
    const path = `M${cx},${cy}c${props.svgAttributes.path.split(/[c|C|L|l|v|V|h|H](.*)/s)[1]}`
    return <path d={path} {...(props.svgAttributes as SVGProps<SVGCircleElement>)} />
  }
  return <circle cx={cx} cy={cy} {...(props.svgAttributes as SVGProps<SVGCircleElement>)} />
}

export function MultiPoint(props: GeometryProps): JSX.Element {
  return (
    <>
      {props.coordinates.map((point, i) => (
        <PointComponent {...props} coordinates={point} key={i} />
      ))}
    </>
  )
}

export function LineString(props: GeometryProps): JSX.Element {
  const { latLngToPixel } = props
  const p =
    'M' +
    (props.coordinates as Array<[number, number]>).reduce((a, [y, x]) => {
      const [v, w] = latLngToPixel([x, y])
      return a + ' ' + v + ' ' + w
    }, '')

  return <path d={p} {...(props.svgAttributes as SVGProps<SVGPathElement>)} />
}

export function MultiLineString(props: GeometryProps): JSX.Element {
  return (
    <>
      {props.coordinates.map((line, i) => (
        <LineString {...props} coordinates={line} key={i} />
      ))}
    </>
  )
}

export function Polygon(props: GeometryProps): JSX.Element {
  const { latLngToPixel } = props
  // GeoJson polygons is a collection of linear rings
  const p = (props.coordinates as Array<Array<[number, number]>>).reduce(
    (a, part) =>
      a +
      ' M' +
      part.reduce((a, [y, x]) => {
        const [v, w] = latLngToPixel([x, y])
        return a + ' ' + v + ' ' + w
      }, '') +
      'Z',
    ''
  )
  return <path d={p} {...(props.svgAttributes as SVGProps<SVGPathElement>)} />
}

export function MultiPolygon(props: GeometryProps): JSX.Element {
  return (
    <>
      {props.coordinates.map((polygon, i) => (
        <Polygon {...props} coordinates={polygon} key={i} />
      ))}
    </>
  )
}

export function GeometryCollection(props: GeometryProps): JSX.Element {
  const renderer = {
    Point: PointComponent,
    MultiPoint,
    LineString,
    MultiLineString,
    Polygon,
    MultiPolygon,
  }

  const { type, coordinates, geometries } = props.geometry

  if (type === 'GeometryCollection') {
    return (
      <>
        {geometries.map((geometry, i) => (
          <GeometryCollection key={i} {...props} geometry={geometry} />
        ))}
      </>
    )
  }

  const Component = renderer[type]

  if (Component === undefined) {
    console.warn(`The GeoJson Type ${type} is not known`)
    return null
  }
  return (
    <Component
      latLngToPixel={props.latLngToPixel}
      geometry={props.geometry}
      coordinates={coordinates}
      svgAttributes={props.svgAttributes}
    />
  )
}

export function GeoJsonFeature(props: GeoJsonProps): JSX.Element {
  const [internalHover, setInternalHover] = useState(props.hover || false)
  const hover = props.hover !== undefined ? props.hover : internalHover
  const callbackSvgAttributes = props.styleCallback && props.styleCallback(props.feature, hover)
  const svgAttributes = callbackSvgAttributes
    ? props.svgAttributes
      ? { ...props.svgAttributes, ...callbackSvgAttributes }
      : callbackSvgAttributes
    : props.svgAttributes
    ? props.svgAttributes
    : defaultSvgAttributes

  const eventParameters = (event: React.MouseEvent<SVGElement>) => ({
    event,
    anchor: props.anchor,
    payload: props.feature,
  })

  return (
    <g
      clipRule="evenodd"
      style={{ pointerEvents: 'auto' }}
      onClick={props.onClick ? (event) => props.onClick(eventParameters(event)) : null}
      onContextMenu={props.onContextMenu ? (event) => props.onContextMenu(eventParameters(event)) : null}
      onMouseOver={(event) => {
        props.onMouseOver && props.onMouseOver(eventParameters(event))
        setInternalHover(true)
      }}
      onMouseOut={(event) => {
        props.onMouseOut && props.onMouseOut(eventParameters(event))
        setInternalHover(false)
      }}
    >
      <GeometryCollection {...props} {...props.feature} svgAttributes={svgAttributes} />
    </g>
  )
}

export function GeoJson(props: GeoJsonProps): JSX.Element {
  const { width, height } = props.mapState

  return (
    <div
      style={{
        position: 'absolute',
        left: '0',
        top: '0',
        pointerEvents: 'none',
        cursor: 'pointer',
        ...(props.style || {}),
      }}
      className={props.className ? `${props.className} pigeon-click-block` : 'pigeon-click-block'}
    >
      <svg
        width={width}
        height={height}
        viewBox={`0 0 ${width} ${height}`}
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        {props.data && props.data.features.map((feature, i) => <GeoJsonFeature key={i} {...props} feature={feature} />)}

        {React.Children.map(props.children, (child) => {
          if (!child) {
            return null
          }

          if (!React.isValidElement(child)) {
            return child
          }

          return React.cloneElement(child, props)
        })}
      </svg>
    </div>
  )
}

export function GeoJsonLoader(props: GeoJsonLoaderProps): JSX.Element {
  const [data, setData] = useState(props.data ? props.data : null)

  useEffect(() => {
    fetch(props.link)
      .then((response) => response.json())
      .then((data) => setData(data))
  }, [props.link])

  return data ? <GeoJson data={data} {...props} /> : null
}