summaryrefslogtreecommitdiff
path: root/node_modules/pigeon-maps/src
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/pigeon-maps/src')
-rw-r--r--node_modules/pigeon-maps/src/controls/ZoomControl.tsx53
-rw-r--r--node_modules/pigeon-maps/src/index.tsx7
-rw-r--r--node_modules/pigeon-maps/src/map/Map.tsx1411
-rw-r--r--node_modules/pigeon-maps/src/overlays/Draggable.tsx181
-rw-r--r--node_modules/pigeon-maps/src/overlays/GeoJson.tsx244
-rw-r--r--node_modules/pigeon-maps/src/overlays/Marker.tsx86
-rw-r--r--node_modules/pigeon-maps/src/overlays/Overlay.tsx23
-rw-r--r--node_modules/pigeon-maps/src/providers.ts24
-rw-r--r--node_modules/pigeon-maps/src/types.ts150
-rw-r--r--node_modules/pigeon-maps/src/utils.ts25
10 files changed, 2204 insertions, 0 deletions
diff --git a/node_modules/pigeon-maps/src/controls/ZoomControl.tsx b/node_modules/pigeon-maps/src/controls/ZoomControl.tsx
new file mode 100644
index 0000000..a98ba39
--- /dev/null
+++ b/node_modules/pigeon-maps/src/controls/ZoomControl.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import { PigeonProps } from '../types'
+
+interface ZoomProps extends PigeonProps {
+ style?: React.CSSProperties
+ buttonStyle?: React.CSSProperties
+}
+
+const commonStyle: React.CSSProperties = {
+ position: 'absolute',
+ top: 10,
+ left: 10,
+}
+
+const commonButtonStyle: React.CSSProperties = {
+ width: 28,
+ height: 28,
+ borderRadius: 2,
+ boxShadow: '0 1px 4px -1px rgba(0,0,0,.3)',
+ background: 'white',
+ lineHeight: '26px',
+ fontSize: '20px',
+ fontWeight: 700,
+ color: '#666',
+ marginBottom: 1,
+ cursor: 'pointer',
+ border: 'none',
+ display: 'block',
+ outline: 'none',
+}
+
+export function ZoomControl({ style, buttonStyle, setCenterZoom, mapState, mapProps }: ZoomProps): JSX.Element {
+ return (
+ <div className="pigeon-zoom-buttons pigeon-drag-block" style={style ? { ...commonStyle, ...style } : commonStyle}>
+ <button
+ className="pigeon-zoom-in"
+ type="button"
+ style={buttonStyle ? { ...commonButtonStyle, ...buttonStyle } : commonButtonStyle}
+ onClick={() => setCenterZoom(mapState.center, Math.min(mapState.zoom + 1, mapProps.maxZoom))}
+ >
+ +
+ </button>
+ <button
+ className="pigeon-zoom-out"
+ type="button"
+ style={buttonStyle ? { ...commonButtonStyle, ...buttonStyle } : commonButtonStyle}
+ onClick={() => setCenterZoom(mapState.center, Math.max(mapState.zoom - 1, mapProps.minZoom))}
+ >
+ –
+ </button>
+ </div>
+ )
+}
diff --git a/node_modules/pigeon-maps/src/index.tsx b/node_modules/pigeon-maps/src/index.tsx
new file mode 100644
index 0000000..78617f9
--- /dev/null
+++ b/node_modules/pigeon-maps/src/index.tsx
@@ -0,0 +1,7 @@
+export * from './types'
+export { Map } from './map/Map'
+export { Marker } from './overlays/Marker'
+export { Overlay } from './overlays/Overlay'
+export { GeoJson, GeoJsonLoader, GeoJsonFeature } from './overlays/GeoJson'
+export { Draggable } from './overlays/Draggable'
+export { ZoomControl } from './controls/ZoomControl'
diff --git a/node_modules/pigeon-maps/src/map/Map.tsx b/node_modules/pigeon-maps/src/map/Map.tsx
new file mode 100644
index 0000000..38a9c26
--- /dev/null
+++ b/node_modules/pigeon-maps/src/map/Map.tsx
@@ -0,0 +1,1411 @@
+import React, { Component } from 'react'
+
+import { debounce, parentPosition, parentHasClass } from '../utils'
+import {
+ Bounds,
+ MapProps,
+ MapReactState,
+ MinMaxBounds,
+ MoveEvent,
+ Point,
+ Tile,
+ TileComponent,
+ TileValues,
+ WAdd,
+ WarningType,
+ WRem,
+} from '../types'
+import { osm } from '../providers'
+
+const ANIMATION_TIME = 300
+const DIAGONAL_THROW_TIME = 1500
+const SCROLL_PIXELS_FOR_ZOOM_LEVEL = 150
+const MIN_DRAG_FOR_THROW = 40
+const CLICK_TOLERANCE = 2
+const DOUBLE_CLICK_DELAY = 300
+const DEBOUNCE_DELAY = 60
+const PINCH_RELEASE_THROW_DELAY = 300
+const WARNING_DISPLAY_TIMEOUT = 300
+
+const NOOP = () => true
+
+// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
+const lng2tile = (lon: number, zoom: number): number => ((lon + 180) / 360) * Math.pow(2, zoom)
+const lat2tile = (lat: number, zoom: number): number =>
+ ((1 - Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) / Math.PI) / 2) *
+ Math.pow(2, zoom)
+
+function tile2lng(x: number, z: number): number {
+ return (x / Math.pow(2, z)) * 360 - 180
+}
+
+function tile2lat(y: number, z: number): number {
+ const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z)
+ return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
+}
+
+function getMousePixel(dom: HTMLElement, event: Pick<MouseEvent, 'clientX' | 'clientY'>): Point {
+ const parent = parentPosition(dom)
+ return [event.clientX - parent.x, event.clientY - parent.y]
+}
+
+function easeOutQuad(t: number): number {
+ return t * (2 - t)
+}
+
+// minLat, maxLat, minLng, maxLng
+const absoluteMinMax = [
+ tile2lat(Math.pow(2, 10), 10),
+ tile2lat(0, 10),
+ tile2lng(0, 10),
+ tile2lng(Math.pow(2, 10), 10),
+] as MinMaxBounds
+
+const hasWindow = typeof window !== 'undefined'
+
+const performanceNow =
+ hasWindow && window.performance && window.performance.now
+ ? () => window.performance.now()
+ : (() => {
+ const timeStart = new Date().getTime()
+ return () => new Date().getTime() - timeStart
+ })()
+
+const requestAnimationFrame = (callback: (timestamp: number) => void): number | null => {
+ if (hasWindow) {
+ return (window.requestAnimationFrame || window.setTimeout)(callback)
+ } else {
+ callback(new Date().getTime())
+ return null
+ }
+}
+const cancelAnimationFrame = (animFrame: number | null) =>
+ hasWindow && animFrame ? (window.cancelAnimationFrame || window.clearTimeout)(animFrame) : false
+
+function srcSet(
+ dprs: number[],
+ url: (x: number, y: number, z: number, dpr?: number) => string,
+ x: number,
+ y: number,
+ z: number
+): string {
+ if (!dprs || dprs.length === 0) {
+ return ''
+ }
+ return dprs.map((dpr) => url(x, y, z, dpr) + (dpr === 1 ? '' : ` ${dpr}x`)).join(', ')
+}
+
+const ImgTile: TileComponent = ({ tile, tileLoaded }) => (
+ <img
+ src={tile.url}
+ srcSet={tile.srcSet}
+ width={tile.width}
+ height={tile.height}
+ loading={'lazy'}
+ onLoad={tileLoaded}
+ alt={''}
+ style={{
+ position: 'absolute',
+ left: tile.left,
+ top: tile.top,
+ willChange: 'transform',
+ transformOrigin: 'top left',
+ opacity: 1,
+ }}
+ />
+)
+
+export class Map extends Component<MapProps, MapReactState> {
+ static defaultProps = {
+ animate: true,
+ metaWheelZoom: false,
+ metaWheelZoomWarning: 'Use META + wheel to zoom!',
+ twoFingerDrag: false,
+ twoFingerDragWarning: 'Use two fingers to move the map',
+ zoomSnap: true,
+ mouseEvents: true,
+ touchEvents: true,
+ warningZIndex: 100,
+ animateMaxScreens: 5,
+ minZoom: 1,
+ maxZoom: 18,
+ limitBounds: 'center',
+ dprs: [],
+ tileComponent: ImgTile,
+ }
+
+ _containerRef?: HTMLDivElement
+ _mousePosition?: Point
+ _loadTracker?: { [key: string]: boolean }
+ _dragStart: Point | null = null
+ _mouseDown = false
+ _moveEvents: MoveEvent[] = []
+ _lastClick: number | null = null
+ _lastTap: number | null = null
+ _lastWheel: number | null = null
+ _touchStartPixel: Point[] | null = null
+ _touchStartMidPoint: Point | null = null
+ _touchStartDistance: number | null = null
+ _secondTouchEnd: number | null = null
+ _warningClearTimeout: number | null = null
+
+ _isAnimating = false
+ _animationStart: number | null = null
+ _animationEnd: number | null = null
+ _zoomStart: number | null = null
+ _centerTarget: Point | null = null
+ _zoomTarget: number | null = null
+ _zoomAround: Point | null = null
+ _animFrame: number | null = null
+
+ _boundsSynced = false
+ _minMaxCache: [number, number, number, MinMaxBounds] | null = null
+
+ _lastZoom: number
+ _lastCenter: Point
+ _centerStart?: Point
+
+ _resizeObserver = null
+
+ constructor(props: MapProps) {
+ super(props)
+
+ this.syncToProps = debounce(this.syncToProps, DEBOUNCE_DELAY)
+
+ // When users are using uncontrolled components we have to keep this
+ // so we can know if we should call onBoundsChanged
+ this._lastZoom = props.defaultZoom ?? props.zoom ?? 14
+ this._lastCenter = props.defaultCenter ?? props.center ?? [0, 0]
+
+ this.state = {
+ zoom: this._lastZoom,
+ center: this._lastCenter,
+ width: props.width ?? props.defaultWidth ?? -1,
+ height: props.height ?? props.defaultHeight ?? -1,
+ zoomDelta: 0,
+ pixelDelta: undefined,
+ oldTiles: [],
+ showWarning: false,
+ warningType: undefined,
+ }
+ }
+
+ componentDidMount(): void {
+ this.props.mouseEvents && this.bindMouseEvents()
+ this.props.touchEvents && this.bindTouchEvents()
+
+ if (!this.props.width || !this.props.height) {
+ // A height:100% container div often results in height=0 being returned on mount.
+ // So ask again once everything is painted.
+ if (!this.updateWidthHeight()) {
+ requestAnimationFrame(this.updateWidthHeight)
+ }
+ this.bindResizeEvent()
+ }
+
+ this.bindWheelEvent()
+ this.syncToProps()
+
+ if (typeof (window as any).ResizeObserver !== 'undefined') {
+ this._resizeObserver = new (window as any).ResizeObserver(() => {
+ this.updateWidthHeight()
+ })
+
+ this._resizeObserver.observe(this._containerRef)
+ }
+ }
+
+ componentWillUnmount(): void {
+ this.props.mouseEvents && this.unbindMouseEvents()
+ this.props.touchEvents && this.unbindTouchEvents()
+
+ this.unbindWheelEvent()
+
+ if (!this.props.width || !this.props.height) {
+ this.unbindResizeEvent()
+ }
+
+ if (this._resizeObserver) {
+ this._resizeObserver.disconnect()
+ }
+ }
+
+ updateWidthHeight = (): boolean => {
+ if (this._containerRef) {
+ const rect = this._containerRef.getBoundingClientRect()
+
+ if (rect && rect.width > 0 && rect.height > 0) {
+ this.setState({
+ width: rect.width,
+ height: rect.height,
+ })
+ return true
+ }
+ }
+ return false
+ }
+
+ wa: WAdd = (...args: Parameters<WAdd>) => window.addEventListener(...args)
+ wr: WRem = (...args: Parameters<WRem>) => window.removeEventListener(...args)
+
+ bindMouseEvents = (): void => {
+ this.wa('mousedown', this.handleMouseDown)
+ this.wa('mouseup', this.handleMouseUp)
+ this.wa('mousemove', this.handleMouseMove)
+ }
+
+ bindTouchEvents = (): void => {
+ this.wa('touchstart', this.handleTouchStart, { passive: false })
+ this.wa('touchmove', this.handleTouchMove, { passive: false })
+ this.wa('touchend', this.handleTouchEnd, { passive: false })
+ }
+
+ unbindMouseEvents = (): void => {
+ this.wr('mousedown', this.handleMouseDown)
+ this.wr('mouseup', this.handleMouseUp)
+ this.wr('mousemove', this.handleMouseMove)
+ }
+
+ unbindTouchEvents = (): void => {
+ this.wr('touchstart', this.handleTouchStart)
+ this.wr('touchmove', this.handleTouchMove)
+ this.wr('touchend', this.handleTouchEnd)
+ }
+
+ bindResizeEvent = (): void => {
+ this.wa('resize', this.updateWidthHeight)
+ }
+
+ unbindResizeEvent = (): void => {
+ this.wr('resize', this.updateWidthHeight)
+ }
+
+ bindWheelEvent = (): void => {
+ if (this._containerRef) {
+ this._containerRef.addEventListener('wheel', this.handleWheel, { passive: false })
+ }
+ }
+
+ unbindWheelEvent = (): void => {
+ if (this._containerRef) {
+ this._containerRef.removeEventListener('wheel', this.handleWheel)
+ }
+ }
+
+ componentDidUpdate(prevProps: MapProps): void {
+ if (this.props.mouseEvents !== prevProps.mouseEvents) {
+ this.props.mouseEvents ? this.bindMouseEvents() : this.unbindMouseEvents()
+ }
+
+ if (this.props.touchEvents !== prevProps.touchEvents) {
+ this.props.touchEvents ? this.bindTouchEvents() : this.unbindTouchEvents()
+ }
+
+ if (this.props.width && this.props.width !== prevProps.width) {
+ this.setState({ width: this.props.width })
+ }
+
+ if (this.props.height && this.props.height !== prevProps.height) {
+ this.setState({ height: this.props.height })
+ }
+
+ if (!this.props.center && !this.props.zoom) {
+ // if the user isn't controlling neither zoom nor center we don't have to update.
+ return
+ }
+ if (
+ (!this.props.center ||
+ (this.props.center[0] === prevProps?.center?.[0] && this.props.center[1] === prevProps.center[1])) &&
+ this.props.zoom === prevProps.zoom
+ ) {
+ // if the user is controlling either zoom or center but nothing changed
+ // we don't have to update aswell
+ return
+ }
+
+ const currentCenter = this._isAnimating ? this._centerTarget : this.state.center
+ const currentZoom = this._isAnimating ? this._zoomTarget : this.state.zoom
+
+ if (currentCenter && currentZoom) {
+ const nextCenter = this.props.center ?? currentCenter // prevent the rare null errors
+ const nextZoom = this.props.zoom ?? currentZoom
+
+ if (
+ Math.abs(nextZoom - currentZoom) > 0.001 ||
+ Math.abs(nextCenter[0] - currentCenter[0]) > 0.0001 ||
+ Math.abs(nextCenter[1] - currentCenter[1]) > 0.0001
+ ) {
+ this.setCenterZoomTarget(nextCenter, nextZoom, true)
+ }
+ }
+ }
+
+ setCenterZoomTarget = (
+ center: Point | null,
+ zoom: number,
+ fromProps = false,
+ zoomAround: Point | null = null,
+ animationDuration = ANIMATION_TIME
+ ): void => {
+ if (
+ this.props.animate &&
+ (!fromProps ||
+ this.distanceInScreens(center, zoom, this.state.center, this.state.zoom) <= this.props.animateMaxScreens)
+ ) {
+ if (this._isAnimating) {
+ cancelAnimationFrame(this._animFrame)
+ const { centerStep, zoomStep } = this.animationStep(performanceNow())
+ this._centerStart = centerStep
+ this._zoomStart = zoomStep
+ } else {
+ this._isAnimating = true
+ this._centerStart = this.limitCenterAtZoom([this._lastCenter[0], this._lastCenter[1]], this._lastZoom)
+ this._zoomStart = this._lastZoom
+ this.onAnimationStart()
+ }
+
+ this._animationStart = performanceNow()
+ this._animationEnd = this._animationStart + animationDuration
+
+ if (zoomAround) {
+ this._zoomAround = zoomAround
+ this._centerTarget = this.calculateZoomCenter(this._lastCenter, zoomAround, this._lastZoom, zoom)
+ } else {
+ this._zoomAround = null
+ this._centerTarget = center
+ }
+ this._zoomTarget = zoom
+
+ this._animFrame = requestAnimationFrame(this.animate)
+ } else {
+ this.stopAnimating()
+
+ if (zoomAround) {
+ const center = this.calculateZoomCenter(this._lastCenter, zoomAround, this._lastZoom, zoom)
+ this.setCenterZoom(center, zoom, fromProps)
+ } else {
+ this.setCenterZoom(center || this.state.center, zoom, fromProps)
+ }
+ }
+ }
+
+ setCenterZoomForChildren = (center: Point | null, zoom: number): void => {
+ this.setCenterZoomTarget(center || this.state.center, zoom || this.state.zoom, true)
+ }
+
+ distanceInScreens = (centerTarget: Point, zoomTarget: number, center: Point, zoom: number): number => {
+ const { width, height } = this.state
+
+ // distance in pixels at the current zoom level
+ const l1 = this.latLngToPixel(center, center, zoom)
+ const l2 = this.latLngToPixel(centerTarget, center, zoom)
+
+ // distance in pixels at the target zoom level (could be the same)
+ const z1 = this.latLngToPixel(center, center, zoomTarget)
+ const z2 = this.latLngToPixel(centerTarget, center, zoomTarget)
+
+ // take the average between the two and divide by width or height to get the distance multiplier in screens
+ const w = (Math.abs(l1[0] - l2[0]) + Math.abs(z1[0] - z2[0])) / 2 / width
+ const h = (Math.abs(l1[1] - l2[1]) + Math.abs(z1[1] - z2[1])) / 2 / height
+
+ // return the distance
+ return Math.sqrt(w * w + h * h)
+ }
+
+ animationStep = (timestamp: number): { centerStep: Point; zoomStep: number } => {
+ if (
+ !this._animationEnd ||
+ !this._animationStart ||
+ !this._zoomTarget ||
+ !this._zoomStart ||
+ !this._centerStart ||
+ !this._centerTarget
+ ) {
+ return {
+ centerStep: this.state.center,
+ zoomStep: this.state.zoom,
+ }
+ }
+ const length = this._animationEnd - this._animationStart
+ const progress = Math.max(timestamp - this._animationStart, 0)
+ const percentage = easeOutQuad(progress / length)
+
+ const zoomDiff = (this._zoomTarget - this._zoomStart) * percentage
+ const zoomStep = this._zoomStart + zoomDiff
+
+ if (this._zoomAround) {
+ const centerStep = this.calculateZoomCenter(this._centerStart, this._zoomAround, this._zoomStart, zoomStep)
+
+ return { centerStep, zoomStep }
+ } else {
+ const centerStep = [
+ this._centerStart[0] + (this._centerTarget[0] - this._centerStart[0]) * percentage,
+ this._centerStart[1] + (this._centerTarget[1] - this._centerStart[1]) * percentage,
+ ] as Point
+
+ return { centerStep, zoomStep }
+ }
+ }
+
+ animate = (timestamp: number): void => {
+ if (!this._animationEnd || timestamp >= this._animationEnd) {
+ this._isAnimating = false
+ this.setCenterZoom(this._centerTarget, this._zoomTarget, true)
+ this.onAnimationStop()
+ } else {
+ const { centerStep, zoomStep } = this.animationStep(timestamp)
+ this.setCenterZoom(centerStep, zoomStep)
+ this._animFrame = requestAnimationFrame(this.animate)
+ }
+ }
+
+ stopAnimating = (): void => {
+ if (this._isAnimating) {
+ this._isAnimating = false
+ this.onAnimationStop()
+ cancelAnimationFrame(this._animFrame)
+ }
+ }
+
+ limitCenterAtZoom = (center?: Point | null, zoom?: number | null): Point => {
+ // [minLat, maxLat, minLng, maxLng]
+ const minMax = this.getBoundsMinMax(zoom || this.state.zoom)
+
+ return [
+ Math.max(Math.min(!center || isNaN(center[0]) ? this.state.center[0] : center[0], minMax[1]), minMax[0]),
+ Math.max(Math.min(!center || isNaN(center[1]) ? this.state.center[1] : center[1], minMax[3]), minMax[2]),
+ ] as Point
+ }
+
+ onAnimationStart = (): void => {
+ this.props.onAnimationStart && this.props.onAnimationStart()
+ }
+
+ onAnimationStop = (): void => {
+ this.props.onAnimationStop && this.props.onAnimationStop()
+ }
+
+ // main logic when changing coordinates
+ setCenterZoom = (center?: Point | null, zoom?: number | null, animationEnded = false): void => {
+ const limitedCenter = this.limitCenterAtZoom(center, zoom)
+
+ if (zoom && Math.round(this.state.zoom) !== Math.round(zoom)) {
+ const tileValues = this.tileValues(this.state)
+ const nextValues = this.tileValues({
+ center: limitedCenter,
+ zoom,
+ width: this.state.width,
+ height: this.state.height,
+ })
+ const oldTiles = this.state.oldTiles
+
+ this.setState(
+ {
+ oldTiles: oldTiles.filter((o) => o.roundedZoom !== tileValues.roundedZoom).concat(tileValues),
+ },
+ NOOP
+ )
+
+ const loadTracker: { [key: string]: boolean } = {}
+
+ for (let x = nextValues.tileMinX; x <= nextValues.tileMaxX; x++) {
+ for (let y = nextValues.tileMinY; y <= nextValues.tileMaxY; y++) {
+ const key = `${x}-${y}-${nextValues.roundedZoom}`
+ loadTracker[key] = false
+ }
+ }
+
+ this._loadTracker = loadTracker
+ }
+
+ this.setState({ center: limitedCenter, zoom: zoom || this.state.zoom }, NOOP)
+
+ const maybeZoom = this.props.zoom ? this.props.zoom : this._lastZoom
+ const maybeCenter = this.props.center ? this.props.center : this._lastCenter
+ if (
+ zoom &&
+ (animationEnded ||
+ Math.abs(maybeZoom - zoom) > 0.001 ||
+ Math.abs(maybeCenter[0] - limitedCenter[0]) > 0.00001 ||
+ Math.abs(maybeCenter[1] - limitedCenter[1]) > 0.00001)
+ ) {
+ this._lastZoom = zoom
+ this._lastCenter = [...limitedCenter]
+ this.syncToProps(limitedCenter, zoom)
+ }
+ }
+
+ getBoundsMinMax = (zoom: number): MinMaxBounds => {
+ if (this.props.limitBounds === 'center') {
+ return absoluteMinMax
+ }
+
+ const { width, height } = this.state
+
+ if (
+ this._minMaxCache &&
+ this._minMaxCache[0] === zoom &&
+ this._minMaxCache[1] === width &&
+ this._minMaxCache[2] === height
+ ) {
+ return this._minMaxCache[3]
+ }
+
+ const pixelsAtZoom = Math.pow(2, zoom) * 256
+
+ const minLng = width > pixelsAtZoom ? 0 : tile2lng(width / 512, zoom) // x
+ const minLat = height > pixelsAtZoom ? 0 : tile2lat(Math.pow(2, zoom) - height / 512, zoom) // y
+
+ const maxLng = width > pixelsAtZoom ? 0 : tile2lng(Math.pow(2, zoom) - width / 512, zoom) // x
+ const maxLat = height > pixelsAtZoom ? 0 : tile2lat(height / 512, zoom) // y
+
+ const minMax = [minLat, maxLat, minLng, maxLng] as MinMaxBounds
+
+ this._minMaxCache = [zoom, width, height, minMax]
+
+ return minMax
+ }
+
+ tileLoaded = (key: string): void => {
+ if (this._loadTracker && key in this._loadTracker) {
+ this._loadTracker[key] = true
+
+ const unloadedCount = Object.values(this._loadTracker).filter((v) => !v).length
+
+ if (unloadedCount === 0) {
+ this.setState({ oldTiles: [] }, NOOP)
+ }
+ }
+ }
+
+ coordsInside(pixel: Point): boolean {
+ const { width, height } = this.state
+
+ if (pixel[0] < 0 || pixel[1] < 0 || pixel[0] >= width || pixel[1] >= height) {
+ return false
+ }
+
+ const parent = this._containerRef
+ if (parent) {
+ const pos = parentPosition(parent)
+ const element = document.elementFromPoint(pixel[0] + pos.x, pixel[1] + pos.y)
+
+ return parent === element || parent.contains(element)
+ } else {
+ return false
+ }
+ }
+
+ handleTouchStart = (event: TouchEvent): void => {
+ if (!this._containerRef) {
+ return
+ }
+ if (event.target && parentHasClass(event.target as HTMLElement, 'pigeon-drag-block')) {
+ return
+ }
+ if (event.touches.length === 1) {
+ const touch = event.touches[0]
+ const pixel = getMousePixel(this._containerRef, touch)
+
+ if (this.coordsInside(pixel)) {
+ this._touchStartPixel = [pixel]
+
+ if (!this.props.twoFingerDrag) {
+ this.stopAnimating()
+
+ if (this._lastTap && performanceNow() - this._lastTap < DOUBLE_CLICK_DELAY) {
+ event.preventDefault()
+ const latLngNow = this.pixelToLatLng(this._touchStartPixel[0])
+ this.setCenterZoomTarget(
+ null,
+ Math.max(this.props.minZoom, Math.min(this.state.zoom + 1, this.props.maxZoom)),
+ false,
+ latLngNow
+ )
+ } else {
+ this._lastTap = performanceNow()
+ this.trackMoveEvents(pixel)
+ }
+ }
+ }
+ // added second finger and first one was in the area
+ } else if (event.touches.length === 2 && this._touchStartPixel) {
+ event.preventDefault()
+
+ this.stopTrackingMoveEvents()
+
+ if (this.state.pixelDelta || this.state.zoomDelta) {
+ this.sendDeltaChange()
+ }
+
+ const t1 = getMousePixel(this._containerRef, event.touches[0])
+ const t2 = getMousePixel(this._containerRef, event.touches[1])
+
+ this._touchStartPixel = [t1, t2]
+ this._touchStartMidPoint = [(t1[0] + t2[0]) / 2, (t1[1] + t2[1]) / 2]
+ this._touchStartDistance = Math.sqrt(Math.pow(t1[0] - t2[0], 2) + Math.pow(t1[1] - t2[1], 2))
+ }
+ }
+
+ handleTouchMove = (event: TouchEvent): void => {
+ if (!this._containerRef) {
+ this._touchStartPixel = null
+ return
+ }
+ if (event.touches.length === 1 && this._touchStartPixel) {
+ const touch = event.touches[0]
+ const pixel = getMousePixel(this._containerRef, touch)
+
+ if (this.props.twoFingerDrag) {
+ if (this.coordsInside(pixel)) {
+ this.showWarning('fingers')
+ }
+ } else {
+ event.preventDefault()
+ this.trackMoveEvents(pixel)
+
+ this.setState(
+ {
+ pixelDelta: [pixel[0] - this._touchStartPixel[0][0], pixel[1] - this._touchStartPixel[0][1]],
+ },
+ NOOP
+ )
+ }
+ } else if (
+ event.touches.length === 2 &&
+ this._touchStartPixel &&
+ this._touchStartMidPoint &&
+ this._touchStartDistance
+ ) {
+ const { width, height, zoom } = this.state
+
+ event.preventDefault()
+
+ const t1 = getMousePixel(this._containerRef, event.touches[0])
+ const t2 = getMousePixel(this._containerRef, event.touches[1])
+
+ const midPoint = [(t1[0] + t2[0]) / 2, (t1[1] + t2[1]) / 2]
+ const midPointDiff = [midPoint[0] - this._touchStartMidPoint[0], midPoint[1] - this._touchStartMidPoint[1]]
+
+ const distance = Math.sqrt(Math.pow(t1[0] - t2[0], 2) + Math.pow(t1[1] - t2[1], 2))
+
+ const zoomDelta =
+ Math.max(
+ this.props.minZoom,
+ Math.min(this.props.maxZoom, zoom + Math.log2(distance / this._touchStartDistance))
+ ) - zoom
+ const scale = Math.pow(2, zoomDelta)
+
+ const centerDiffDiff = [(width / 2 - midPoint[0]) * (scale - 1), (height / 2 - midPoint[1]) * (scale - 1)]
+
+ this.setState(
+ {
+ zoomDelta: zoomDelta,
+ pixelDelta: [centerDiffDiff[0] + midPointDiff[0] * scale, centerDiffDiff[1] + midPointDiff[1] * scale],
+ },
+ NOOP
+ )
+ }
+ }
+
+ handleTouchEnd = (event: TouchEvent): void => {
+ if (!this._containerRef) {
+ this._touchStartPixel = null
+ return
+ }
+ if (this._touchStartPixel) {
+ const { zoomSnap, twoFingerDrag, minZoom, maxZoom } = this.props
+ const { zoomDelta } = this.state
+ const { center, zoom } = this.sendDeltaChange()
+
+ if (event.touches.length === 0) {
+ if (twoFingerDrag) {
+ this.clearWarning()
+ } else {
+ // if the click started and ended at about
+ // the same place we can view it as a click
+ // and not prevent default behavior.
+ const oldTouchPixel = this._touchStartPixel[0]
+ const newTouchPixel = getMousePixel(this._containerRef, event.changedTouches[0])
+
+ if (
+ Math.abs(oldTouchPixel[0] - newTouchPixel[0]) > CLICK_TOLERANCE ||
+ Math.abs(oldTouchPixel[1] - newTouchPixel[1]) > CLICK_TOLERANCE
+ ) {
+ // don't throw immediately after releasing the second finger
+ if (!this._secondTouchEnd || performanceNow() - this._secondTouchEnd > PINCH_RELEASE_THROW_DELAY) {
+ event.preventDefault()
+ this.throwAfterMoving(newTouchPixel, center, zoom)
+ }
+ }
+
+ this._touchStartPixel = null
+ this._secondTouchEnd = null
+ }
+ } else if (event.touches.length === 1) {
+ event.preventDefault()
+ const touch = getMousePixel(this._containerRef, event.touches[0])
+
+ this._secondTouchEnd = performanceNow()
+ this._touchStartPixel = [touch]
+ this.trackMoveEvents(touch)
+
+ if (zoomSnap) {
+ // if somehow we have no midpoint for the two finger touch, just take the center of the map
+ const latLng = this._touchStartMidPoint ? this.pixelToLatLng(this._touchStartMidPoint) : this.state.center
+
+ let zoomTarget
+
+ // do not zoom up/down if we must drag with 2 fingers and didn't change the zoom level
+ if (twoFingerDrag && Math.round(this.state.zoom) === Math.round(this.state.zoom + zoomDelta)) {
+ zoomTarget = Math.round(this.state.zoom)
+ } else {
+ zoomTarget = zoomDelta > 0 ? Math.ceil(this.state.zoom) : Math.floor(this.state.zoom)
+ }
+ const zoom = Math.max(minZoom, Math.min(zoomTarget, maxZoom))
+
+ this.setCenterZoomTarget(latLng, zoom, false, latLng)
+ }
+ }
+ }
+ }
+
+ handleMouseDown = (event: MouseEvent): void => {
+ if (!this._containerRef) {
+ return
+ }
+ const pixel = getMousePixel(this._containerRef, event)
+
+ if (
+ event.button === 0 &&
+ (!event.target || !parentHasClass(event.target as HTMLElement, 'pigeon-drag-block')) &&
+ this.coordsInside(pixel)
+ ) {
+ this.stopAnimating()
+ event.preventDefault()
+
+ if (this._lastClick && performanceNow() - this._lastClick < DOUBLE_CLICK_DELAY) {
+ if (!parentHasClass(event.target as HTMLElement, 'pigeon-click-block')) {
+ const latLngNow = this.pixelToLatLng(this._mousePosition || pixel)
+ this.setCenterZoomTarget(
+ null,
+ Math.max(this.props.minZoom, Math.min(this.state.zoom + 1, this.props.maxZoom)),
+ false,
+ latLngNow
+ )
+ }
+ } else {
+ this._lastClick = performanceNow()
+
+ this._mouseDown = true
+ this._dragStart = pixel
+ this.trackMoveEvents(pixel)
+ }
+ }
+ }
+
+ handleMouseMove = (event: MouseEvent): void => {
+ if (!this._containerRef) {
+ return
+ }
+ this._mousePosition = getMousePixel(this._containerRef, event)
+
+ if (this._mouseDown && this._dragStart) {
+ this.trackMoveEvents(this._mousePosition)
+ this.setState(
+ {
+ pixelDelta: [this._mousePosition[0] - this._dragStart[0], this._mousePosition[1] - this._dragStart[1]],
+ },
+ NOOP
+ )
+ }
+ }
+
+ handleMouseUp = (event: MouseEvent): void => {
+ if (!this._containerRef) {
+ this._mouseDown = false
+ return
+ }
+ const { pixelDelta } = this.state
+
+ if (this._mouseDown) {
+ this._mouseDown = false
+
+ const pixel = getMousePixel(this._containerRef, event)
+
+ if (
+ this.props.onClick &&
+ (!event.target || !parentHasClass(event.target as HTMLElement, 'pigeon-click-block')) &&
+ (!pixelDelta || Math.abs(pixelDelta[0]) + Math.abs(pixelDelta[1]) <= CLICK_TOLERANCE)
+ ) {
+ const latLng = this.pixelToLatLng(pixel)
+ this.props.onClick({ event, latLng, pixel })
+ this.setState({ pixelDelta: undefined }, NOOP)
+ } else {
+ const { center, zoom } = this.sendDeltaChange()
+
+ this.throwAfterMoving(pixel, center, zoom)
+ }
+ }
+ }
+
+ // https://www.bennadel.com/blog/1856-using-jquery-s-animate-step-callback-function-to-create-custom-animations.htm
+ stopTrackingMoveEvents = (): void => {
+ this._moveEvents = []
+ }
+
+ trackMoveEvents = (coords: Point): void => {
+ const timestamp = performanceNow()
+
+ if (this._moveEvents.length === 0 || timestamp - this._moveEvents[this._moveEvents.length - 1].timestamp > 40) {
+ this._moveEvents.push({ timestamp, coords })
+ if (this._moveEvents.length > 2) {
+ this._moveEvents.shift()
+ }
+ }
+ }
+
+ throwAfterMoving = (coords: Point, center: Point, zoom: number): void => {
+ const { width, height } = this.state
+ const { animate } = this.props
+
+ const timestamp = performanceNow()
+ const lastEvent = this._moveEvents.shift()
+
+ if (lastEvent && animate) {
+ const deltaMs = Math.max(timestamp - lastEvent.timestamp, 1)
+
+ const delta = [
+ ((coords[0] - lastEvent.coords[0]) / deltaMs) * 120,
+ ((coords[1] - lastEvent.coords[1]) / deltaMs) * 120,
+ ]
+
+ const distance = Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1])
+
+ if (distance > MIN_DRAG_FOR_THROW) {
+ const diagonal = Math.sqrt(width * width + height * height)
+
+ const throwTime = (DIAGONAL_THROW_TIME * distance) / diagonal
+
+ const lng = tile2lng(lng2tile(center[1], zoom) - delta[0] / 256.0, zoom)
+ const lat = tile2lat(lat2tile(center[0], zoom) - delta[1] / 256.0, zoom)
+
+ this.setCenterZoomTarget([lat, lng], zoom, false, null, throwTime)
+ }
+ }
+
+ this.stopTrackingMoveEvents()
+ }
+
+ sendDeltaChange = () => {
+ const { center, zoom, pixelDelta, zoomDelta } = this.state
+
+ let lat = center[0]
+ let lng = center[1]
+
+ if (pixelDelta || zoomDelta !== 0) {
+ lng = tile2lng(lng2tile(center[1], zoom + zoomDelta) - (pixelDelta ? pixelDelta[0] / 256.0 : 0), zoom + zoomDelta)
+ lat = tile2lat(lat2tile(center[0], zoom + zoomDelta) - (pixelDelta ? pixelDelta[1] / 256.0 : 0), zoom + zoomDelta)
+ this.setCenterZoom([lat, lng], zoom + zoomDelta)
+ }
+
+ this.setState(
+ {
+ pixelDelta: undefined,
+ zoomDelta: 0,
+ },
+ NOOP
+ )
+
+ return {
+ center: this.limitCenterAtZoom([lat, lng], zoom + zoomDelta),
+ zoom: zoom + zoomDelta,
+ }
+ }
+
+ getBounds = (center = this.state.center, zoom = this.zoomPlusDelta()): Bounds => {
+ const { width, height } = this.state
+
+ return {
+ ne: this.pixelToLatLng([width - 1, 0], center, zoom),
+ sw: this.pixelToLatLng([0, height - 1], center, zoom),
+ }
+ }
+
+ syncToProps = (center = this.state.center, zoom = this.state.zoom): void => {
+ const { onBoundsChanged } = this.props
+
+ if (onBoundsChanged) {
+ const bounds = this.getBounds(center, zoom)
+
+ onBoundsChanged({ center, zoom, bounds, initial: !this._boundsSynced })
+
+ this._boundsSynced = true
+ }
+ }
+
+ handleWheel = (event: WheelEvent): void => {
+ const { mouseEvents, metaWheelZoom, zoomSnap, animate } = this.props
+
+ if (!mouseEvents) {
+ return
+ }
+
+ if (!metaWheelZoom || event.metaKey || event.ctrlKey) {
+ event.preventDefault()
+
+ const addToZoom = -event.deltaY / SCROLL_PIXELS_FOR_ZOOM_LEVEL
+
+ if (!zoomSnap && this._zoomTarget) {
+ const stillToAdd = this._zoomTarget - this.state.zoom
+ this.zoomAroundMouse(addToZoom + stillToAdd, event)
+ } else {
+ if (animate) {
+ this.zoomAroundMouse(addToZoom, event)
+ } else {
+ if (!this._lastWheel || performanceNow() - this._lastWheel > ANIMATION_TIME) {
+ this._lastWheel = performanceNow()
+ this.zoomAroundMouse(addToZoom, event)
+ }
+ }
+ }
+ } else {
+ this.showWarning('wheel')
+ }
+ }
+
+ showWarning = (warningType: WarningType): void => {
+ if (!this.state.showWarning || this.state.warningType !== warningType) {
+ this.setState({ showWarning: true, warningType })
+ }
+
+ if (this._warningClearTimeout) {
+ window.clearTimeout(this._warningClearTimeout)
+ }
+ this._warningClearTimeout = window.setTimeout(this.clearWarning, WARNING_DISPLAY_TIMEOUT)
+ }
+
+ clearWarning = (): void => {
+ if (this.state.showWarning) {
+ this.setState({ showWarning: false })
+ }
+ }
+
+ zoomAroundMouse = (zoomDiff: number, event: MouseEvent): void => {
+ if (!this._containerRef) {
+ return
+ }
+ const { zoom } = this.state
+ const { minZoom, maxZoom, zoomSnap } = this.props
+
+ this._mousePosition = getMousePixel(this._containerRef, event)
+
+ if (!this._mousePosition || (zoom === minZoom && zoomDiff < 0) || (zoom === maxZoom && zoomDiff > 0)) {
+ return
+ }
+
+ const latLngNow = this.pixelToLatLng(this._mousePosition)
+
+ let zoomTarget = zoom + zoomDiff
+ if (zoomSnap) {
+ zoomTarget = zoomDiff < 0 ? Math.floor(zoomTarget) : Math.ceil(zoomTarget)
+ }
+ zoomTarget = Math.max(minZoom, Math.min(zoomTarget, maxZoom))
+
+ this.setCenterZoomTarget(null, zoomTarget, false, latLngNow)
+ }
+
+ // tools
+
+ zoomPlusDelta = (): number => {
+ return this.state.zoom + this.state.zoomDelta
+ }
+
+ pixelToLatLng = (pixel: Point, center = this.state.center, zoom = this.zoomPlusDelta()): Point => {
+ const { width, height, pixelDelta } = this.state
+
+ const pointDiff = [
+ (pixel[0] - width / 2 - (pixelDelta ? pixelDelta[0] : 0)) / 256.0,
+ (pixel[1] - height / 2 - (pixelDelta ? pixelDelta[1] : 0)) / 256.0,
+ ]
+
+ const tileX = lng2tile(center[1], zoom) + pointDiff[0]
+ const tileY = lat2tile(center[0], zoom) + pointDiff[1]
+
+ return [
+ Math.max(absoluteMinMax[0], Math.min(absoluteMinMax[1], tile2lat(tileY, zoom))),
+ Math.max(absoluteMinMax[2], Math.min(absoluteMinMax[3], tile2lng(tileX, zoom))),
+ ] as Point
+ }
+
+ latLngToPixel = (latLng: Point, center = this.state.center, zoom = this.zoomPlusDelta()): Point => {
+ const { width, height, pixelDelta } = this.state
+
+ const tileCenterX = lng2tile(center[1], zoom)
+ const tileCenterY = lat2tile(center[0], zoom)
+
+ const tileX = lng2tile(latLng[1], zoom)
+ const tileY = lat2tile(latLng[0], zoom)
+
+ return [
+ (tileX - tileCenterX) * 256.0 + width / 2 + (pixelDelta ? pixelDelta[0] : 0),
+ (tileY - tileCenterY) * 256.0 + height / 2 + (pixelDelta ? pixelDelta[1] : 0),
+ ] as Point
+ }
+
+ calculateZoomCenter = (center: Point, coords: Point, oldZoom: number, newZoom: number): Point => {
+ const { width, height } = this.state
+
+ const pixelBefore = this.latLngToPixel(coords, center, oldZoom)
+ const pixelAfter = this.latLngToPixel(coords, center, newZoom)
+
+ const newCenter = this.pixelToLatLng(
+ [width / 2 + pixelAfter[0] - pixelBefore[0], height / 2 + pixelAfter[1] - pixelBefore[1]],
+ center,
+ newZoom
+ )
+
+ return this.limitCenterAtZoom(newCenter, newZoom)
+ }
+
+ // ref
+
+ setRef = (dom: HTMLDivElement) => {
+ this._containerRef = dom
+ }
+
+ // data to display the tiles
+
+ tileValues({
+ center,
+ zoom,
+ pixelDelta,
+ zoomDelta,
+ width,
+ height,
+ }: {
+ center: Point
+ zoom: number
+ pixelDelta?: Point
+ zoomDelta?: number
+ width: number
+ height: number
+ }): TileValues {
+ const roundedZoom = Math.round(zoom + (zoomDelta || 0))
+ const zoomDiff = zoom + (zoomDelta || 0) - roundedZoom
+
+ const scale = Math.pow(2, zoomDiff)
+ const scaleWidth = width / scale
+ const scaleHeight = height / scale
+
+ const tileCenterX = lng2tile(center[1], roundedZoom) - (pixelDelta ? pixelDelta[0] / 256.0 / scale : 0)
+ const tileCenterY = lat2tile(center[0], roundedZoom) - (pixelDelta ? pixelDelta[1] / 256.0 / scale : 0)
+
+ const halfWidth = scaleWidth / 2 / 256.0
+ const halfHeight = scaleHeight / 2 / 256.0
+
+ const tileMinX = Math.floor(tileCenterX - halfWidth)
+ const tileMaxX = Math.floor(tileCenterX + halfWidth)
+
+ const tileMinY = Math.floor(tileCenterY - halfHeight)
+ const tileMaxY = Math.floor(tileCenterY + halfHeight)
+
+ return {
+ tileMinX,
+ tileMaxX,
+ tileMinY,
+ tileMaxY,
+ tileCenterX,
+ tileCenterY,
+ roundedZoom,
+ zoomDelta: zoomDelta || 0,
+ scaleWidth,
+ scaleHeight,
+ scale,
+ }
+ }
+
+ // display the tiles
+
+ renderTiles(): JSX.Element {
+ const { oldTiles, width, height } = this.state
+ const { dprs } = this.props
+ const mapUrl = this.props.provider || osm
+
+ const {
+ tileMinX,
+ tileMaxX,
+ tileMinY,
+ tileMaxY,
+ tileCenterX,
+ tileCenterY,
+ roundedZoom,
+ scaleWidth,
+ scaleHeight,
+ scale,
+ } = this.tileValues(this.state)
+
+ const tiles: Tile[] = []
+
+ for (let i = 0; i < oldTiles.length; i++) {
+ const old = oldTiles[i]
+ const zoomDiff = old.roundedZoom - roundedZoom
+
+ if (Math.abs(zoomDiff) > 4 || zoomDiff === 0) {
+ continue
+ }
+
+ const pow = 1 / Math.pow(2, zoomDiff)
+ const xDiff = -(tileMinX - old.tileMinX * pow) * 256
+ const yDiff = -(tileMinY - old.tileMinY * pow) * 256
+
+ const xMin = Math.max(old.tileMinX, 0)
+ const yMin = Math.max(old.tileMinY, 0)
+ const xMax = Math.min(old.tileMaxX, Math.pow(2, old.roundedZoom) - 1)
+ const yMax = Math.min(old.tileMaxY, Math.pow(2, old.roundedZoom) - 1)
+
+ for (let x = xMin; x <= xMax; x++) {
+ for (let y = yMin; y <= yMax; y++) {
+ tiles.push({
+ key: `${x}-${y}-${old.roundedZoom}`,
+ url: mapUrl(x, y, old.roundedZoom),
+ srcSet: srcSet(dprs, mapUrl, x, y, old.roundedZoom),
+ left: xDiff + (x - old.tileMinX) * 256 * pow,
+ top: yDiff + (y - old.tileMinY) * 256 * pow,
+ width: 256 * pow,
+ height: 256 * pow,
+ active: false,
+ })
+ }
+ }
+ }
+
+ const xMin = Math.max(tileMinX, 0)
+ const yMin = Math.max(tileMinY, 0)
+ const xMax = Math.min(tileMaxX, Math.pow(2, roundedZoom) - 1)
+ const yMax = Math.min(tileMaxY, Math.pow(2, roundedZoom) - 1)
+
+ for (let x = xMin; x <= xMax; x++) {
+ for (let y = yMin; y <= yMax; y++) {
+ tiles.push({
+ key: `${x}-${y}-${roundedZoom}`,
+ url: mapUrl(x, y, roundedZoom),
+ srcSet: srcSet(dprs, mapUrl, x, y, roundedZoom),
+ left: (x - tileMinX) * 256,
+ top: (y - tileMinY) * 256,
+ width: 256,
+ height: 256,
+ active: true,
+ })
+ }
+ }
+
+ const boxStyle: React.CSSProperties = {
+ width: scaleWidth,
+ height: scaleHeight,
+ position: 'absolute',
+ top: `calc((100% - ${height}px) / 2)`,
+ left: `calc((100% - ${width}px) / 2)`,
+ overflow: 'hidden',
+ willChange: 'transform',
+ transform: `scale(${scale}, ${scale})`,
+ transformOrigin: 'top left',
+ }
+ const boxClassname = this.props.boxClassname || 'pigeon-tiles-box'
+
+ const left = -((tileCenterX - tileMinX) * 256 - scaleWidth / 2)
+ const top = -((tileCenterY - tileMinY) * 256 - scaleHeight / 2)
+
+ const tilesStyle: React.CSSProperties = {
+ position: 'absolute',
+ width: (tileMaxX - tileMinX + 1) * 256,
+ height: (tileMaxY - tileMinY + 1) * 256,
+ willChange: 'transform',
+ transform: `translate(${left}px, ${top}px)`,
+ }
+
+ const Tile = this.props.tileComponent
+
+ return (
+ <div style={boxStyle} className={boxClassname}>
+ <div className="pigeon-tiles" style={tilesStyle}>
+ {tiles.map((tile) => (
+ <Tile key={tile.key} tile={tile} tileLoaded={() => this.tileLoaded(tile.key)} />
+ ))}
+ </div>
+ </div>
+ )
+ }
+
+ renderOverlays(): JSX.Element {
+ const { width, height, center } = this.state
+
+ const mapState = {
+ bounds: this.getBounds(),
+ zoom: this.zoomPlusDelta(),
+ center: center,
+ width,
+ height,
+ }
+
+ const childrenWithProps = React.Children.map(this.props.children, (child) => {
+ if (!child) {
+ return null
+ }
+
+ if (!React.isValidElement(child)) {
+ return child
+ }
+
+ const { anchor, position, offset } = child.props
+
+ const c = this.latLngToPixel(anchor || position || center)
+
+ return React.cloneElement(child, {
+ left: c[0] - (offset ? offset[0] : 0),
+ top: c[1] - (offset ? offset[1] : 0),
+ latLngToPixel: this.latLngToPixel,
+ pixelToLatLng: this.pixelToLatLng,
+ setCenterZoom: this.setCenterZoomForChildren,
+ mapProps: this.props,
+ mapState,
+ })
+ })
+
+ const childrenStyle: React.CSSProperties = {
+ position: 'absolute',
+ width: width,
+ height: height,
+ top: `calc((100% - ${height}px) / 2)`,
+ left: `calc((100% - ${width}px) / 2)`,
+ }
+
+ return (
+ <div className="pigeon-overlays" style={childrenStyle}>
+ {childrenWithProps}
+ </div>
+ )
+ }
+
+ renderAttribution(): JSX.Element | null {
+ const { attribution, attributionPrefix } = this.props
+
+ if (attribution === false) {
+ return null
+ }
+
+ const style: React.CSSProperties = {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ fontSize: '11px',
+ padding: '2px 5px',
+ background: 'rgba(255, 255, 255, 0.7)',
+ fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
+ color: '#333',
+ }
+
+ const linkStyle: React.CSSProperties = {
+ color: '#0078A8',
+ textDecoration: 'none',
+ }
+
+ return (
+ <div key="attr" className="pigeon-attribution" style={style}>
+ {attributionPrefix === false ? null : (
+ <span>
+ {attributionPrefix || (
+ <a href="https://pigeon-maps.js.org/" style={linkStyle} target="_blank" rel="noreferrer noopener">
+ Pigeon
+ </a>
+ )}
+ {' | '}
+ </span>
+ )}
+ {attribution || (
+ <span>
+ {' © '}
+ <a
+ href="https://www.openstreetmap.org/copyright"
+ style={linkStyle}
+ target="_blank"
+ rel="noreferrer noopener"
+ >
+ OpenStreetMap
+ </a>
+ {' contributors'}
+ </span>
+ )}
+ </div>
+ )
+ }
+
+ renderWarning(): JSX.Element | null {
+ const { metaWheelZoom, metaWheelZoomWarning, twoFingerDrag, twoFingerDragWarning, warningZIndex } = this.props
+ const { showWarning, warningType, width, height } = this.state
+
+ if ((metaWheelZoom && metaWheelZoomWarning) || (twoFingerDrag && twoFingerDragWarning)) {
+ const style: React.CSSProperties = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: width,
+ height: height,
+ overflow: 'hidden',
+ pointerEvents: 'none',
+ opacity: showWarning ? 100 : 0,
+ transition: 'opacity 300ms',
+ background: 'rgba(0,0,0,0.5)',
+ color: '#fff',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ fontSize: 22,
+ fontFamily: '"Arial", sans-serif',
+ textAlign: 'center',
+ zIndex: warningZIndex,
+ }
+
+ const meta =
+ typeof window !== 'undefined' && window.navigator && window.navigator.platform.toUpperCase().indexOf('MAC') >= 0
+ ? '⌘'
+ : 'ctrl'
+
+ const warningText = warningType === 'fingers' ? twoFingerDragWarning : metaWheelZoomWarning
+
+ return (
+ <div className="pigeon-overlay-warning" style={style}>
+ {warningText.replace('META', meta)}
+ </div>
+ )
+ } else {
+ return null
+ }
+ }
+
+ render(): JSX.Element {
+ const { touchEvents, twoFingerDrag } = this.props
+ const { width, height } = this.state
+
+ const containerStyle: React.CSSProperties = {
+ width: this.props.width ? width : '100%',
+ height: this.props.height ? height : '100%',
+ position: 'relative',
+ display: 'inline-block',
+ overflow: 'hidden',
+ background: '#dddddd',
+ touchAction: touchEvents ? (twoFingerDrag ? 'pan-x pan-y' : 'none') : 'auto',
+ }
+
+ const hasSize = !!(width && height)
+
+ return (
+ <div style={containerStyle} ref={this.setRef} dir="ltr">
+ {hasSize && this.renderTiles()}
+ {hasSize && this.renderOverlays()}
+ {hasSize && this.renderAttribution()}
+ {hasSize && this.renderWarning()}
+ </div>
+ )
+ }
+}
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>
+ )
+}
diff --git a/node_modules/pigeon-maps/src/providers.ts b/node_modules/pigeon-maps/src/providers.ts
new file mode 100644
index 0000000..5a11046
--- /dev/null
+++ b/node_modules/pigeon-maps/src/providers.ts
@@ -0,0 +1,24 @@
+export function osm(x: number, y: number, z: number): string {
+ const s = String.fromCharCode(97 + ((x + y + z) % 3))
+ return `https://tile.openstreetmap.org/${z}/${x}/${y}.png`
+}
+
+export function stamenToner(x: number, y: number, z: number, dpr = 1): string {
+ return `https://stamen-tiles.a.ssl.fastly.net/toner/${z}/${x}/${y}${dpr >= 2 ? '@2x' : ''}.png`
+}
+
+export function stamenTerrain(x: number, y: number, z: number, dpr = 1): string {
+ return `https://stamen-tiles.a.ssl.fastly.net/terrain/${z}/${x}/${y}${dpr >= 2 ? '@2x' : ''}.jpg`
+}
+
+export const maptiler =
+ (apiKey: string, map = 'streets') =>
+ (x: number, y: number, z: number, dpr = 1): string => {
+ return `https://api.maptiler.com/maps/${map}/256/${z}/${x}/${y}${dpr >= 2 ? '@2x' : ''}.png?key=${apiKey}`
+ }
+
+export const stadiamaps =
+ (style = 'alidade_smooth') =>
+ (x: number, y: number, z: number, dpr = 1): string => {
+ return `https://tiles.stadiamaps.com/styles/${style}/${z}/${x}/${y}${dpr >= 2 ? '@2x' : ''}.png`
+ }
diff --git a/node_modules/pigeon-maps/src/types.ts b/node_modules/pigeon-maps/src/types.ts
new file mode 100644
index 0000000..2bee710
--- /dev/null
+++ b/node_modules/pigeon-maps/src/types.ts
@@ -0,0 +1,150 @@
+import React from 'react'
+
+/** @description `[latitude, longitude]` */
+export type Point = [number, number]
+
+/** @description `{ ne: [latitude, longitude], sw: [latitude, longitude] }` */
+export interface Bounds {
+ ne: [number, number]
+ sw: [number, number]
+}
+
+export interface MapProps {
+ center?: Point
+ defaultCenter?: Point
+
+ zoom?: number
+ defaultZoom?: number
+
+ width?: number
+ defaultWidth?: number
+
+ height?: number
+ defaultHeight?: number
+
+ provider?: (x: number, y: number, z: number, dpr?: number) => string
+ dprs?: number[]
+ children?: React.ReactNode
+
+ animate?: boolean
+ animateMaxScreens?: number
+
+ minZoom?: number
+ maxZoom?: number
+
+ metaWheelZoom?: boolean
+ metaWheelZoomWarning?: string
+ twoFingerDrag?: boolean
+ twoFingerDragWarning?: string
+ warningZIndex?: number
+
+ attribution?: JSX.Element | false
+ attributionPrefix?: JSX.Element | false
+
+ zoomSnap?: boolean
+ mouseEvents?: boolean
+ touchEvents?: boolean
+
+ onClick?: ({ event, latLng, pixel }: { event: MouseEvent; latLng: [number, number]; pixel: [number, number] }) => void
+ onBoundsChanged?: ({
+ center,
+ zoom,
+ bounds,
+ initial,
+ }: {
+ center: [number, number]
+ bounds: Bounds
+ zoom: number
+ initial: boolean
+ }) => void
+ onAnimationStart?: () => void
+ onAnimationStop?: () => void
+
+ // will be set to "edge" from v0.12 onward, defaulted to "center" before
+ limitBounds?: 'center' | 'edge'
+
+ boxClassname?: string
+ tileComponent?: TileComponent
+}
+
+export type TileComponent = (props: TileComponentProps) => JSX.Element
+
+export interface TileComponentProps {
+ tile: Tile
+ tileLoaded: () => void
+}
+
+export interface Tile {
+ key: string
+ url: string
+ srcSet: string
+ left: number
+ top: number
+ width: number
+ height: number
+ active: boolean
+}
+
+export interface TileValues {
+ tileMinX: number
+ tileMaxX: number
+ tileMinY: number
+ tileMaxY: number
+ tileCenterX: number
+ tileCenterY: number
+ roundedZoom: number
+ zoomDelta: number
+ scaleWidth: number
+ scaleHeight: number
+ scale: number
+}
+
+export type WarningType = 'fingers' | 'wheel'
+
+export type WAdd = typeof window.addEventListener
+export type WRem = typeof window.removeEventListener
+
+export interface MoveEvent {
+ timestamp: number
+ coords: Point
+}
+
+type MinLat = number
+type MaxLat = number
+type MinLng = number
+type MaxLng = number
+export type MinMaxBounds = [MinLat, MaxLat, MinLng, MaxLng]
+
+export interface MapReactState {
+ zoom: number
+ center: Point
+ width: number
+ height: number
+ zoomDelta: number
+ pixelDelta?: [number, number]
+ oldTiles: TileValues[]
+ showWarning: boolean
+ warningType?: WarningType
+}
+
+export interface MapState {
+ bounds: Bounds
+ zoom: number
+ center: Point
+ width: number
+ height: number
+}
+
+export interface PigeonProps {
+ anchor?: Point
+ offset?: Point
+ left?: number
+ top?: number
+ mapState?: MapState
+ mapProps?: MapProps
+
+ // pigeon functions
+ latLngToPixel?: (latLng: Point, center?: Point, zoom?: number) => Point
+ pixelToLatLng?: (pixel: Point, center?: Point, zoom?: number) => Point
+ setCenterZoom?: (center: Point | null, zoom: number, zoomAround?: Point | null, animationDuration?: number) => void
+}
diff --git a/node_modules/pigeon-maps/src/utils.ts b/node_modules/pigeon-maps/src/utils.ts
new file mode 100644
index 0000000..ae80350
--- /dev/null
+++ b/node_modules/pigeon-maps/src/utils.ts
@@ -0,0 +1,25 @@
+export function debounce<T extends (...args: any[]) => any>(func: T, wait: number) {
+ let timeout: ReturnType<typeof setTimeout>
+ return function (this: any, ...args: Parameters<T>) {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const context = this
+ clearTimeout(timeout)
+ timeout = setTimeout(() => func.apply(context, args), wait)
+ }
+}
+
+export function parentHasClass(element: HTMLElement, className: string) {
+ while (element) {
+ if (element.classList && element.classList.contains(className)) {
+ return true
+ }
+ element = element.parentElement
+ }
+
+ return false
+}
+
+export function parentPosition(element: HTMLElement) {
+ const rect = element.getBoundingClientRect()
+ return { x: rect.left, y: rect.top }
+}