diff options
author | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-08-18 13:29:54 +0300 |
---|---|---|
committer | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-08-18 13:29:54 +0300 |
commit | fd70f95224374d23157ee7c0357733102cd0df53 (patch) | |
tree | e490c12e021cedaf211b292d5d623baa32a673fc /node_modules/pigeon-maps/src/overlays |
Diffstat (limited to 'node_modules/pigeon-maps/src/overlays')
-rw-r--r-- | node_modules/pigeon-maps/src/overlays/Draggable.tsx | 181 | ||||
-rw-r--r-- | node_modules/pigeon-maps/src/overlays/GeoJson.tsx | 244 | ||||
-rw-r--r-- | node_modules/pigeon-maps/src/overlays/Marker.tsx | 86 | ||||
-rw-r--r-- | node_modules/pigeon-maps/src/overlays/Overlay.tsx | 23 |
4 files changed, 534 insertions, 0 deletions
diff --git a/node_modules/pigeon-maps/src/overlays/Draggable.tsx b/node_modules/pigeon-maps/src/overlays/Draggable.tsx new file mode 100644 index 0000000..4cac3c6 --- /dev/null +++ b/node_modules/pigeon-maps/src/overlays/Draggable.tsx @@ -0,0 +1,181 @@ +import React, { useEffect, useRef, useState } from 'react' +import { PigeonProps, Point } from '../types' + +function isDescendentOf(element, ancestor) { + while (element) { + if (element === ancestor) { + return true + } + element = element.parentElement + } + + return false +} + +interface DraggableProps extends PigeonProps { + className?: string + style?: React.CSSProperties + + children?: React.ReactNode + + onDragStart?: (anchor: Point) => void + onDragMove?: (anchor: Point) => void + onDragEnd?: (anchor: Point) => void +} + +interface DraggableState { + isDragging: boolean + startX?: number + startY?: number + startLeft?: number + startTop?: number + deltaX: number + deltaY: number +} + +const defaultState: DraggableState = { + isDragging: false, + startX: undefined, + startY: undefined, + startLeft: undefined, + startTop: undefined, + deltaX: 0, + deltaY: 0, +} + +export function Draggable(props: DraggableProps): JSX.Element { + const dragRef = useRef<HTMLDivElement>() + const propsRef = useRef<DraggableProps>(props) + const stateRef = useRef({ ...defaultState }) + const [_state, _setState] = useState(defaultState) + + propsRef.current = props + + const setState = (stateUpdate: Partial<DraggableState>): void => { + const newState = { ...stateRef.current, ...stateUpdate } + stateRef.current = newState + _setState(newState) + } + + const { mouseEvents, touchEvents } = props.mapProps + + useEffect(() => { + const handleDragStart = (event: MouseEvent | TouchEvent) => { + if (isDescendentOf(event.target, dragRef.current)) { + event.preventDefault() + + setState({ + isDragging: true, + startX: ('touches' in event ? event.touches[0] : event).clientX, + startY: ('touches' in event ? event.touches[0] : event).clientY, + startLeft: propsRef.current.left, + startTop: propsRef.current.top, + deltaX: 0, + deltaY: 0, + }) + + if (propsRef.current.onDragStart) { + const { left, top, offset, pixelToLatLng } = propsRef.current + propsRef.current.onDragMove(pixelToLatLng([left + (offset ? offset[0] : 0), top + (offset ? offset[1] : 0)])) + } + } + } + + const handleDragMove = (event: MouseEvent | TouchEvent) => { + if (!stateRef.current.isDragging) { + return + } + + event.preventDefault() + + const x = ('touches' in event ? event.touches[0] : event).clientX + const y = ('touches' in event ? event.touches[0] : event).clientY + + const deltaX = x - stateRef.current.startX + const deltaY = y - stateRef.current.startY + + setState({ deltaX, deltaY }) + + if (propsRef.current.onDragMove) { + const { offset, pixelToLatLng } = propsRef.current + const { startLeft, startTop } = stateRef.current + + propsRef.current.onDragMove( + pixelToLatLng([startLeft + deltaX + (offset ? offset[0] : 0), startTop + deltaY + (offset ? offset[1] : 0)]) + ) + } + } + + const handleDragEnd = (event: MouseEvent | TouchEvent) => { + if (!stateRef.current.isDragging) { + return + } + + event.preventDefault() + + const { offset, pixelToLatLng } = propsRef.current + const { deltaX, deltaY, startLeft, startTop } = stateRef.current + + propsRef.current.onDragEnd?.( + pixelToLatLng([startLeft + deltaX + (offset ? offset[0] : 0), startTop + deltaY + (offset ? offset[1] : 0)]) + ) + + setState({ + isDragging: false, + startX: undefined, + startY: undefined, + startLeft: undefined, + startTop: undefined, + deltaX: 0, + deltaY: 0, + }) + } + + const wa = (e: string, t: EventListener, o?: AddEventListenerOptions) => window.addEventListener(e, t, o) + const wr = (e: string, t: EventListener) => window.removeEventListener(e, t) + + if (mouseEvents) { + wa('mousedown', handleDragStart) + wa('mousemove', handleDragMove) + wa('mouseup', handleDragEnd) + } + + if (touchEvents) { + wa('touchstart', handleDragStart, { passive: false }) + wa('touchmove', handleDragMove, { passive: false }) + wa('touchend', handleDragEnd, { passive: false }) + } + + return () => { + if (mouseEvents) { + wr('mousedown', handleDragStart) + wr('mousemove', handleDragMove) + wr('mouseup', handleDragEnd) + } + + if (touchEvents) { + wr('touchstart', handleDragStart) + wr('touchmove', handleDragMove) + wr('touchend', handleDragEnd) + } + } + }, [mouseEvents, touchEvents]) + + const { left, top, className, style } = props + const { deltaX, deltaY, startLeft, startTop, isDragging } = _state + + return ( + <div + style={{ + cursor: isDragging ? 'grabbing' : 'grab', + ...(style || {}), + position: 'absolute', + transform: `translate(${isDragging ? startLeft + deltaX : left}px, ${isDragging ? startTop + deltaY : top}px)`, + }} + ref={dragRef} + className={`pigeon-drag-block${className ? ` ${className}` : ''}`} + > + {props.children} + </div> + ) +} diff --git a/node_modules/pigeon-maps/src/overlays/GeoJson.tsx b/node_modules/pigeon-maps/src/overlays/GeoJson.tsx new file mode 100644 index 0000000..d25cfe6 --- /dev/null +++ b/node_modules/pigeon-maps/src/overlays/GeoJson.tsx @@ -0,0 +1,244 @@ +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 +} diff --git a/node_modules/pigeon-maps/src/overlays/Marker.tsx b/node_modules/pigeon-maps/src/overlays/Marker.tsx new file mode 100644 index 0000000..f1970c2 --- /dev/null +++ b/node_modules/pigeon-maps/src/overlays/Marker.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react' +import { PigeonProps } from '../types' + +interface MarkerProps extends PigeonProps { + color?: string + payload?: any + + width?: number + height?: number + + // optional modifiers + hover?: boolean + style?: React.CSSProperties + className?: string + + children?: JSX.Element + + // 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 +} + +export function Marker(props: MarkerProps): JSX.Element { + const width = + typeof props.width !== 'undefined' + ? props.width + : typeof props.height !== 'undefined' + ? (props.height * 29) / 34 + : 29 + const height = + typeof props.height !== 'undefined' + ? props.height + : typeof props.width !== 'undefined' + ? (props.width * 34) / 29 + : 34 + const [internalHover, setInternalHover] = useState(props.hover || false) + const hover = typeof props.hover === 'undefined' ? internalHover : props.hover + const color = props.color || '#93C0D0' + + // what do you expect to get back with the event + const eventParameters = (event: React.MouseEvent) => ({ + event, + anchor: props.anchor, + payload: props.payload, + }) + + return ( + <div + style={{ + position: 'absolute', + transform: `translate(${props.left - width / 2}px, ${props.top - (height - 1)}px)`, + filter: hover ? 'drop-shadow(0 0 4px rgba(0, 0, 0, .3))' : '', + pointerEvents: 'none', + cursor: 'pointer', + ...(props.style || {}), + }} + className={props.className ? `${props.className} pigeon-click-block` : 'pigeon-click-block'} + 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) + }} + > + {props.children || ( + <svg width={width} height={height} viewBox="0 0 61 71" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g style={{ pointerEvents: 'auto' }}> + <path + d="M52 31.5C52 36.8395 49.18 42.314 45.0107 47.6094C40.8672 52.872 35.619 57.678 31.1763 61.6922C30.7916 62.0398 30.2084 62.0398 29.8237 61.6922C25.381 57.678 20.1328 52.872 15.9893 47.6094C11.82 42.314 9 36.8395 9 31.5C9 18.5709 18.6801 9 30.5 9C42.3199 9 52 18.5709 52 31.5Z" + fill={color} + stroke="white" + strokeWidth="4" + /> + <circle cx="30.5" cy="30.5" r="8.5" fill="white" opacity={hover ? 0.98 : 0.6} /> + </g> + </svg> + )} + </div> + ) +} diff --git a/node_modules/pigeon-maps/src/overlays/Overlay.tsx b/node_modules/pigeon-maps/src/overlays/Overlay.tsx new file mode 100644 index 0000000..0bc8ecd --- /dev/null +++ b/node_modules/pigeon-maps/src/overlays/Overlay.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { PigeonProps } from '../types' + +interface OverlayProps extends PigeonProps { + style?: React.CSSProperties + className?: string + children?: React.ReactNode +} + +export function Overlay(props: OverlayProps) { + return ( + <div + style={{ + position: 'absolute', + transform: `translate(${props.left}px, ${props.top}px)`, + ...(props.style || {}), + }} + className={props.className ? `${props.className} pigeon-click-block` : 'pigeon-click-block'} + > + {props.children} + </div> + ) +} |