UNPKG

react-mapfilter

Version:

These components are designed for viewing data in Mapeo. They share a common interface:

239 lines (220 loc) 7.91 kB
import "core-js/modules/es.array.iterator"; import "core-js/modules/web.dom-collections.iterator"; import _extends from "@babel/runtime-corejs3/helpers/extends"; import _findInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/find"; // @flow import React, { useState, useMemo, useCallback, useRef, useEffect, useImperativeHandle } from 'react'; import { useIntl, IntlProvider } from 'react-intl'; import ReactMapboxGl from 'react-mapbox-gl'; import mapboxgl from 'mapbox-gl'; /*:: import type { Observation } from 'mapeo-schema'*/ import { getLastImage } from '../utils/helpers'; import { makeStyles } from '@material-ui/core/styles'; import ObservationLayer from './ObservationLayer'; import Popup from './Popup'; /*:: import type { CameraOptions, CommonViewContentProps } from '../types'*/ /*:: export type MapViewContentProps = { /** Called with * [CameraOptions](https://docs.mapbox.com/mapbox-gl-js/api/#cameraoptions) * with properties `center`, `zoom`, `bearing`, `pitch` *-/ onMapMove?: CameraOptions => any, /** Initial position of the map - an object with properties `center`, `zoom`, * `bearing`, `pitch`. If this is not set then the map will by default zoom to * the bounds of the observations. If you are going to unmount and re-mount * this component (e.g. within tabs) then you will want to use onMove to store * the position in state, and pass it as initialPosition for when the map * re-mounts. *-/ initialMapPosition?: $Shape<CameraOptions>, /** Mapbox access token *-/ mapboxAccessToken: string, /** Mapbox style url *-/ mapStyle?: any }*/ /*:: type Props = { ...$Exact<MapViewContentProps>, ...$Exact<CommonViewContentProps>, print?: boolean }*/ /*:: export type MapInstance = { fitBounds: () => any, flyTo: () => any }*/ const useStyles = makeStyles({ container: { flex: 1 }, '@global': { // The "Improve Map" link does not work when Mapeo Desktop is used offline, // and since the data the user is looking at is mainly data that is not in // OpenStreetMap, this link does not make much sense to the user. '.mapbox-improve-map': { display: 'none' } } }); const fitBoundsOptions = { duration: 0, padding: 10 }; const noop = () => {}; const MapViewContent = ({ observations, mapboxAccessToken, getPreset, getMedia, onClick, initialMapPosition = {}, onMapMove = noop, mapStyle = 'mapbox://styles/mapbox/outdoors-v10', print = false } /*: Props*/ , ref) => { const map = useRef(); const classes = useStyles(); const intl = useIntl(); const [hovered, setHovered] = useState(null); const [styleLoaded, setStyleLoaded] = useState(false); useImperativeHandle(ref, () => ({ fitBounds: (...args) => { if (!map.current) return; map.current.fitBounds.apply(map.current, args); }, flyTo: (...args) => { if (!map.current) return; map.current.flyTo.apply(map.current, args); } })); // We don't want to change the map viewport if the observations array changes, // which it will do if the filter changes. We only set the bounds for the very // initial render, and only if initialMapPosition zoom and center are not set. const initialBounds = useMemo(() => initialMapPosition.center == null && initialMapPosition.zoom == null ? getBounds(observations) : undefined, // eslint-disable-next-line react-hooks/exhaustive-deps []); useEffect(() => { if (!map.current || !observations || !observations.length || map.current.__hasMoved || initialMapPosition.center != null && initialMapPosition.zoom != null) return; map.current.__hasMoved = true; const bounds = getBounds(observations); map.current.flyTo({ center: [bounds[0][0] + (bounds[1][0] - bounds[0][0]) / 2, bounds[0][1] + (bounds[1][1] - bounds[0][1]) / 2], zoom: 9, bearing: 0, pitch: 0 }); }, [initialMapPosition.center, initialMapPosition.zoom, observations, styleLoaded]); // We don't allow the map to be a controlled component - position can only be // set when the map is initially mounted and after that state is internal const position = useMemo(() => { const { center, zoom, bearing, pitch } = initialMapPosition; const bounds = getBounds(observations); // initialMapPosition overrides default behaviour of fitting the map to the // bounds of the observations, but if any properties of initialMapPosition are // we set some default values return { center: center || [bounds[0][0] + (bounds[1][0] - bounds[0][0]) / 2, bounds[0][1] + (bounds[1][1] - bounds[0][1]) / 2], zoom: zoom ? [zoom] : [9], bearing: bearing ? [bearing] : [0], pitch: pitch ? [pitch] : [0] }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const Mapbox = useMemo(() => ReactMapboxGl({ accessToken: mapboxAccessToken, dragRotate: false, pitchWithRotate: false, attributionControl: false, logoPosition: 'bottom-right', scrollZoom: !print, injectCSS: false }), [mapboxAccessToken, print]); const handleStyleLoad = useCallback(mapInstance => { mapInstance.addControl(new mapboxgl.NavigationControl({})); mapInstance.addControl(new mapboxgl.ScaleControl({})); mapInstance.addControl(new mapboxgl.AttributionControl({ compact: true })); map.current = mapInstance; setStyleLoaded(true); }, []); const handleMouseMove = useCallback(e => { if (e.features.length === 0) return setHovered(null); const obs = _findInstanceProperty(observations).call(observations, obs => obs.id === e.features[0].properties.id); setHovered(obs); }, [observations]); const handleMouseLeave = useCallback(e => { setHovered(null); }, []); const handleMapMove = useCallback((map, e) => { onMapMove({ center: map.getCenter().toArray(), zoom: map.getZoom(), bearing: map.getBearing(), pitch: map.getPitch() }); }, [onMapMove]); function getLastImageUrl(observation /*: Observation*/ ) /*: string | void*/ { const lastImageAttachment = getLastImage(observation); if (!lastImageAttachment) return; const media = getMedia(lastImageAttachment, { width: Popup.imageSize, height: Popup.imageSize }); if (media) return media.src; } function getName(observation /*: Observation*/ ) /*: string*/ { const preset = getPreset(observation); return preset && preset.name || 'Observation'; } return /*#__PURE__*/React.createElement(Mapbox, _extends({ style: mapStyle, className: classes.container, fitBounds: initialBounds, fitBoundsOptions: fitBoundsOptions, onStyleLoad: handleStyleLoad, onMove: handleMapMove }, position), /*#__PURE__*/React.createElement(ObservationLayer, { observations: observations, onClick: onClick, onMouseLeave: handleMouseLeave, onMouseMove: handleMouseMove, print: print }), hovered && /*#__PURE__*/React.createElement(Popup, { imageUrl: getLastImageUrl(hovered), title: getName(hovered), subtitle: intl.formatTime(hovered.created_at, { year: 'numeric', month: 'long', day: '2-digit' }), coordinates: // $FlowFixMe - these are always non-nullish when on a map [hovered.lon, hovered.lat] })); }; export default /*#__PURE__*/React.forwardRef(MapViewContent); function getBounds(observations /*: Observation[]*/ ) /*: [[number, number], [number, number]]*/ { const extent = [[-180, -85], [180, 85]]; for (const { lat, lon } of observations) { if (lon == null || lat == null) continue; if (extent[0][0] < lon) extent[0][0] = lon; if (extent[0][1] < lat) extent[0][1] = lat; if (extent[1][0] > lon) extent[1][0] = lon; if (extent[1][1] > lat) extent[1][1] = lat; } return extent; } //# sourceMappingURL=MapViewContent.js.map