react-leaflet-marker-layer
Version:
A custom layer for react-leaflet that makes plotting react components simple
178 lines (147 loc) • 4.7 kB
JavaScript
import React from 'react';
import ReactDOM from 'react-dom';
import map from 'lodash.map';
import forEach from 'lodash.foreach';
import pluck from 'lodash.pluck';
import min from 'lodash.min';
import max from 'lodash.max';
import isNumber from 'lodash.isnumber';
import filter from 'lodash.filter';
import L from 'leaflet';
import { MapLayer } from 'react-leaflet';
import shouldPureComponentUpdate from 'react-pure-render/function';
export type LngLat = {
lng: number;
lat: number;
}
export type Point = {
x: number;
y: number;
}
export type Map = {
layerPointToLatLng: (lngLat: Point) => LngLat;
latLngToLayerPoint: (lngLat: LngLat) => Point;
on: (event: string, handler: () => void) => void;
getPanes: () => Panes;
invalidateSize: () => void;
}
export type Panes = {
overlayPane: Pane;
}
export type Pane = {
appendChild: (element: Object) => void;
}
function isInvalid(num: number): boolean {
return !isNumber(num) && !num;
}
function isValid(num: number): boolean {
return !isInvalid(num);
}
function shouldIgnoreLocation(loc: LngLat): boolean {
return isInvalid(loc.lng) || isInvalid(loc.lat);
}
export default class MarkerLayer extends MapLayer {
static propTypes = {
markers: React.PropTypes.array,
longitudeExtractor: React.PropTypes.func.isRequired,
latitudeExtractor: React.PropTypes.func.isRequired,
markerComponent: React.PropTypes.func.isRequired,
propsForMarkers: React.PropTypes.object,
fitBoundsOnLoad: React.PropTypes.bool,
fitBoundsOnUpdate: React.PropTypes.bool,
};
shouldComponentUpdate = shouldPureComponentUpdate;
componentWillReceiveProps() {
// no-op to override MapLayer behavior
}
componentDidMount(): void {
this.leafletElement = ReactDOM.findDOMNode(this.refs.container);
this.props.map.getPanes().overlayPane.appendChild(this.leafletElement);
if (this.props.fitBoundsOnLoad) {
this.fitBounds();
}
this.attachEvents();
this.updatePosition();
}
componentWillUnmount(): void {
this.props.map.getPanes().overlayPane.removeChild(this.leafletElement);
}
fitBounds(): void {
const markers = this.props.markers;
const lngs = filter(map(markers, this.props.longitudeExtractor), isValid);
const lats = filter(map(markers, this.props.latitudeExtractor), isValid);
const ne = { lng: max(lngs), lat: max(lats) };
const sw = { lng: min(lngs), lat: min(lats) };
if (shouldIgnoreLocation(ne) || shouldIgnoreLocation(sw)) {
return;
}
this.props.map.fitBounds(L.latLngBounds(L.latLng(sw), L.latLng(ne)));
}
markersOrPositionExtractorsChanged(props): boolean {
return this.props.markers !== props.markers
|| this.props.longitudeExtractor !== props.longitudeExtractor
|| this.props.latitudeExtractor !== props.latitudeExtractor;
}
componentDidUpdate(prevProps): void {
this.props.map.invalidateSize();
if (this.props.fitBoundsOnUpdate && this.markersOrPositionExtractorsChanged(prevProps)) {
this.fitBounds();
}
this.updatePosition();
}
attachEvents(): void {
const map: Map = this.props.map;
map.on('viewreset', () => this.updatePosition());
}
getLocationForMarker(marker): LngLat {
return {
lat: this.props.latitudeExtractor(marker),
lng: this.props.longitudeExtractor(marker)
};
}
updatePosition(): void {
forEach(this.props.markers, (marker, i) => {
const markerElement = ReactDOM.findDOMNode(
this.refs[this.getMarkerRefName(i)]
);
const location = this.getLocationForMarker(marker);
if (shouldIgnoreLocation(location)) {
return;
}
const point = this.props.map.latLngToLayerPoint(L.latLng(location));
L.DomUtil.setPosition(markerElement, point);
});
}
render(): React.Element {
return (
<div ref="container"
className={`leaflet-objects-pane
leaflet-marker-pane
leaflet-zoom-hide
react-leaflet-marker-layer`} >
{this.renderMarkers()}
</div>
);
}
renderMarkers(): Array<React.Element> {
const style = { position: 'absolute' };
const MarkerComponent = this.props.markerComponent;
return map(this.props.markers, (marker, index: number) => {
if (shouldIgnoreLocation(this.getLocationForMarker(marker))) {
return;
}
return (
<MarkerComponent
{...this.props.propsForMarkers}
key={index}
style={style}
map={this.props.map}
ref={this.getMarkerRefName(index)}
marker={marker} />
);
});
}
getMarkerRefName(index: number): string {
return `marker-${index}`;
}
}