382 lines
9.5 KiB
Plaintext
382 lines
9.5 KiB
Plaintext
// @flow
|
|
|
|
import {
|
|
latLngBounds,
|
|
Map as LeafletMap,
|
|
type CRS,
|
|
type Renderer,
|
|
} from 'leaflet'
|
|
import React, { type Node } from 'react'
|
|
|
|
import { LeafletProvider } from './context'
|
|
import MapEvented from './MapEvented'
|
|
import updateClassName from './utils/updateClassName'
|
|
import omit from './utils/omit'
|
|
import type {
|
|
LatLng,
|
|
LatLngBounds,
|
|
LeafletContext,
|
|
Point,
|
|
Viewport,
|
|
} from './types'
|
|
|
|
const OTHER_PROPS = [
|
|
'children',
|
|
'className',
|
|
'id',
|
|
'style',
|
|
'useFlyTo',
|
|
'whenReady',
|
|
]
|
|
|
|
const normalizeCenter = (pos: LatLng): [number, number] => {
|
|
return Array.isArray(pos)
|
|
? [pos[0], pos[1]]
|
|
: [pos.lat, pos.lon ? pos.lon : pos.lng]
|
|
}
|
|
|
|
type LeafletElement = LeafletMap
|
|
|
|
type ZoomOption = boolean | 'center'
|
|
type Props = {
|
|
[key: string]: any,
|
|
// Leaflet options
|
|
preferCanvas?: boolean,
|
|
attributionControl?: boolean,
|
|
zoomControl?: boolean,
|
|
closePopupOnClick?: boolean,
|
|
zoomSnap?: number,
|
|
zoomDelta?: number,
|
|
trackResize?: boolean,
|
|
boxZoom?: boolean,
|
|
doubleClickZoom?: ZoomOption,
|
|
dragging?: boolean,
|
|
crs?: CRS,
|
|
center?: LatLng,
|
|
zoom?: number,
|
|
minZoom?: number,
|
|
maxZoom?: number,
|
|
maxBounds?: LatLngBounds,
|
|
renderer?: Renderer,
|
|
zoomAnimation?: boolean,
|
|
zoomAnimationThreshold?: number,
|
|
fadeAnimation?: boolean,
|
|
markerZoomAnimation?: boolean,
|
|
transform3DLimit?: number,
|
|
inertia?: boolean,
|
|
inertiaDeceleration?: number,
|
|
inertiaMaxSpeed?: number,
|
|
easeLinearity?: number,
|
|
worldCopyJump?: boolean,
|
|
maxBoundsViscosity?: number,
|
|
keyboard?: boolean,
|
|
keyboardPanDelta?: number,
|
|
scrollWheelZoom?: ZoomOption,
|
|
wheelDebounceTime?: number,
|
|
wheelPxPerZoomLevel?: number,
|
|
tap?: boolean,
|
|
tapTolerance?: number,
|
|
touchZoom?: ZoomOption,
|
|
bounceAtZoomLimits?: boolean,
|
|
// Additional options
|
|
animate?: boolean,
|
|
duration?: number,
|
|
noMoveStart?: boolean,
|
|
bounds?: LatLngBounds,
|
|
boundsOptions?: {|
|
|
paddingTopLeft?: Point,
|
|
paddingBottomRight?: Point,
|
|
padding?: Point,
|
|
maxZoom?: number,
|
|
|},
|
|
children: Node,
|
|
className?: string,
|
|
id?: string,
|
|
style?: Object,
|
|
useFlyTo?: boolean,
|
|
viewport?: Viewport,
|
|
whenReady?: () => void,
|
|
}
|
|
|
|
export default class Map extends MapEvented<LeafletElement, Props> {
|
|
className: ?string
|
|
contextValue: ?LeafletContext
|
|
container: ?HTMLDivElement
|
|
viewport: Viewport = {
|
|
center: undefined,
|
|
zoom: undefined,
|
|
}
|
|
|
|
_ready: boolean = false
|
|
_updating: boolean = false
|
|
|
|
constructor(props: Props) {
|
|
super(props)
|
|
this.className = props.className
|
|
}
|
|
|
|
createLeafletElement(props: Props): LeafletElement {
|
|
const { viewport, ...options } = props
|
|
if (viewport) {
|
|
if (viewport.center) {
|
|
options.center = viewport.center
|
|
}
|
|
if (typeof viewport.zoom === 'number') {
|
|
options.zoom = viewport.zoom
|
|
}
|
|
}
|
|
return new LeafletMap(this.container, options)
|
|
}
|
|
|
|
updateLeafletElement(fromProps: Props, toProps: Props) {
|
|
this._updating = true
|
|
|
|
const {
|
|
bounds,
|
|
boundsOptions,
|
|
boxZoom,
|
|
center,
|
|
className,
|
|
doubleClickZoom,
|
|
dragging,
|
|
keyboard,
|
|
maxBounds,
|
|
scrollWheelZoom,
|
|
tap,
|
|
touchZoom,
|
|
useFlyTo,
|
|
viewport,
|
|
zoom,
|
|
} = toProps
|
|
|
|
updateClassName(this.container, fromProps.className, className)
|
|
|
|
if (viewport && viewport !== fromProps.viewport) {
|
|
const c = viewport.center ? viewport.center : center
|
|
const z = viewport.zoom == null ? zoom : viewport.zoom
|
|
if (useFlyTo === true) {
|
|
this.leafletElement.flyTo(c, z, this.getZoomPanOptions(toProps))
|
|
} else {
|
|
this.leafletElement.setView(c, z, this.getZoomPanOptions(toProps))
|
|
}
|
|
} else if (center && this.shouldUpdateCenter(center, fromProps.center)) {
|
|
if (useFlyTo === true) {
|
|
this.leafletElement.flyTo(center, zoom, this.getZoomPanOptions(toProps))
|
|
} else {
|
|
this.leafletElement.setView(
|
|
center,
|
|
zoom,
|
|
this.getZoomPanOptions(toProps),
|
|
)
|
|
}
|
|
} else if (typeof zoom === 'number' && zoom !== fromProps.zoom) {
|
|
if (fromProps.zoom == null) {
|
|
this.leafletElement.setView(
|
|
center,
|
|
zoom,
|
|
this.getZoomPanOptions(toProps),
|
|
)
|
|
} else {
|
|
this.leafletElement.setZoom(zoom, this.getZoomPanOptions(toProps))
|
|
}
|
|
}
|
|
|
|
if (maxBounds && this.shouldUpdateBounds(maxBounds, fromProps.maxBounds)) {
|
|
this.leafletElement.setMaxBounds(maxBounds)
|
|
}
|
|
|
|
if (
|
|
bounds &&
|
|
(this.shouldUpdateBounds(bounds, fromProps.bounds) ||
|
|
boundsOptions !== fromProps.boundsOptions)
|
|
) {
|
|
if (useFlyTo === true) {
|
|
this.leafletElement.flyToBounds(
|
|
bounds,
|
|
this.getFitBoundsOptions(toProps),
|
|
)
|
|
} else {
|
|
this.leafletElement.fitBounds(bounds, this.getFitBoundsOptions(toProps))
|
|
}
|
|
}
|
|
|
|
if (boxZoom !== fromProps.boxZoom) {
|
|
if (boxZoom === true) {
|
|
this.leafletElement.boxZoom.enable()
|
|
} else {
|
|
this.leafletElement.boxZoom.disable()
|
|
}
|
|
}
|
|
|
|
if (doubleClickZoom !== fromProps.doubleClickZoom) {
|
|
if (doubleClickZoom === true || typeof doubleClickZoom === 'string') {
|
|
this.leafletElement.options.doubleClickZoom = doubleClickZoom
|
|
this.leafletElement.doubleClickZoom.enable()
|
|
} else {
|
|
this.leafletElement.doubleClickZoom.disable()
|
|
}
|
|
}
|
|
|
|
if (dragging !== fromProps.dragging) {
|
|
if (dragging === true) {
|
|
this.leafletElement.dragging.enable()
|
|
} else {
|
|
this.leafletElement.dragging.disable()
|
|
}
|
|
}
|
|
|
|
if (keyboard !== fromProps.keyboard) {
|
|
if (keyboard === true) {
|
|
this.leafletElement.keyboard.enable()
|
|
} else {
|
|
this.leafletElement.keyboard.disable()
|
|
}
|
|
}
|
|
|
|
if (scrollWheelZoom !== fromProps.scrollWheelZoom) {
|
|
if (scrollWheelZoom === true || typeof scrollWheelZoom === 'string') {
|
|
this.leafletElement.options.scrollWheelZoom = scrollWheelZoom
|
|
this.leafletElement.scrollWheelZoom.enable()
|
|
} else {
|
|
this.leafletElement.scrollWheelZoom.disable()
|
|
}
|
|
}
|
|
|
|
if (tap !== fromProps.tap) {
|
|
if (tap === true) {
|
|
this.leafletElement.tap.enable()
|
|
} else {
|
|
this.leafletElement.tap.disable()
|
|
}
|
|
}
|
|
|
|
if (touchZoom !== fromProps.touchZoom) {
|
|
if (touchZoom === true || typeof touchZoom === 'string') {
|
|
this.leafletElement.options.touchZoom = touchZoom
|
|
this.leafletElement.touchZoom.enable()
|
|
} else {
|
|
this.leafletElement.touchZoom.disable()
|
|
}
|
|
}
|
|
|
|
this._updating = false
|
|
}
|
|
|
|
getZoomPanOptions(props: Props) {
|
|
const { animate, duration, easeLinearity, noMoveStart } = props
|
|
return {
|
|
animate,
|
|
duration,
|
|
easeLinearity,
|
|
noMoveStart,
|
|
}
|
|
}
|
|
|
|
getFitBoundsOptions(props: Props) {
|
|
const zoomPanOptions = this.getZoomPanOptions(props)
|
|
return {
|
|
...zoomPanOptions,
|
|
...props.boundsOptions,
|
|
}
|
|
}
|
|
|
|
onViewportChange = () => {
|
|
const center = this.leafletElement.getCenter()
|
|
this.viewport = {
|
|
center: center ? [center.lat, center.lng] : undefined,
|
|
zoom: this.leafletElement.getZoom(),
|
|
}
|
|
if (this.props.onViewportChange && !this._updating) {
|
|
this.props.onViewportChange(this.viewport)
|
|
}
|
|
}
|
|
|
|
onViewportChanged = () => {
|
|
if (this.props.onViewportChanged && !this._updating) {
|
|
this.props.onViewportChanged(this.viewport)
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
const props = omit(this.props, ...OTHER_PROPS)
|
|
this.leafletElement = this.createLeafletElement(props)
|
|
|
|
this.leafletElement.on('move', this.onViewportChange)
|
|
this.leafletElement.on('moveend', this.onViewportChanged)
|
|
|
|
if (props.bounds != null) {
|
|
this.leafletElement.fitBounds(
|
|
props.bounds,
|
|
this.getFitBoundsOptions(props),
|
|
)
|
|
}
|
|
|
|
this.contextValue = {
|
|
layerContainer: this.leafletElement,
|
|
map: this.leafletElement,
|
|
}
|
|
|
|
super.componentDidMount()
|
|
this.forceUpdate() // Re-render now that leafletElement is created
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
if (this._ready === false) {
|
|
this._ready = true
|
|
if (this.props.whenReady) {
|
|
this.leafletElement.whenReady(this.props.whenReady)
|
|
}
|
|
}
|
|
|
|
super.componentDidUpdate(prevProps)
|
|
this.updateLeafletElement(prevProps, this.props)
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
super.componentWillUnmount()
|
|
|
|
this.leafletElement.off('move', this.onViewportChange)
|
|
this.leafletElement.off('moveend', this.onViewportChanged)
|
|
|
|
// The canvas renderer uses requestAnimationFrame, making a deferred call to a deleted object
|
|
// When preferCanvas is set, use simpler teardown logic
|
|
if (this.props.preferCanvas === true) {
|
|
this.leafletElement._initEvents(true)
|
|
this.leafletElement._stop()
|
|
} else {
|
|
this.leafletElement.remove()
|
|
}
|
|
}
|
|
|
|
bindContainer = (container: ?HTMLDivElement): void => {
|
|
this.container = container
|
|
}
|
|
|
|
shouldUpdateCenter(next: LatLng, prev: LatLng) {
|
|
if (!prev) return true
|
|
next = normalizeCenter(next)
|
|
prev = normalizeCenter(prev)
|
|
return next[0] !== prev[0] || next[1] !== prev[1]
|
|
}
|
|
|
|
shouldUpdateBounds(next: LatLngBounds, prev: LatLngBounds) {
|
|
return prev ? !latLngBounds(next).equals(latLngBounds(prev)) : true
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
className={this.className}
|
|
id={this.props.id}
|
|
ref={this.bindContainer}
|
|
style={this.props.style}>
|
|
{this.contextValue ? (
|
|
<LeafletProvider value={this.contextValue}>
|
|
{this.props.children}
|
|
</LeafletProvider>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
}
|