import { useContext, useEffect, useState } from 'react' import { MapContainer, TileLayer, Polyline, Circle, useMap } from 'react-leaflet' import Marker from 'react-leaflet-enhanced-marker' import { Box } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import AppContext from '../../AppContext' import MyLocationIcon from '@material-ui/icons/MyLocation' const ChangeMapCenter = ( {center, zoom} ) => { const map = useMap() map.flyTo(checkPosition(center)) return <></> } const SelfCircle = () => { const { geolocation, geoPermission } = useContext ( AppContext ) if ( geoPermission !== 'granted' ) { return null } return ( <Circle center={checkPosition(geolocation)} radius={25} /> ) } const CenterControl = ( {onClick}) => { const classes = useStyles() return ( <div className='leaflet-bottom leaflet-right'> <div className={`${classes.centerControlContainer} leaflet-control leaflet-bar`} onClick={onClick} > <MyLocationIcon className={classes.centerControl} /> </div> </div> ) } const RouteMap = ({stops, stopIdx, onMarkerClick}) => { const { stopList, geoPermission, setGeoPermission, setGeolocation } = useContext ( AppContext ) const classes = useStyles() const [center, setCenter] = useState(stopList[stops[stopIdx]] ? stopList[stops[stopIdx]].location : {}) const [map, setMap] = useState(null) useEffect ( () => { setCenter(stopList[stops[stopIdx]] ? stopList[stops[stopIdx]].location : stopList[stops[Math.round(stops.length/2)]].location ) // eslint-disable-next-line react-hooks/exhaustive-deps }, [stopIdx]) const updateCenter = (e) => { setCenter(map.getCenter()) } useEffect ( () => { if ( !map ) return map.on('dragend', updateCenter) return () => { map.off('dragend', updateCenter) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [map]) return ( <Box className={`${classes.mapContainer}`}> <MapContainer center={checkPosition(center)} zoom={16} scrollWheelZoom={false} className={classes.mapContainer} whenCreated={setMap} > <ChangeMapCenter center={checkPosition(center)} zoom={16} /> <TileLayer attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>' url="https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png" /> { // plot stops stops.map((stopId, idx, self) => <Marker key={`${stopId}-${idx}`} position={stopList[stopId].location} icon={<BusStopMarker active={idx === stopIdx} passed={idx < stopIdx} />} eventHandlers={{ click: (e) => {onMarkerClick(idx)(e, true, true)} }} /> ) } { // plot line stops.slice(1).map((stopId, idx) => <Polyline key={`${stopId}-line`} positions={[ getPoint(stopList[stops[idx]].location), getPoint(stopList[stopId].location) ]} color={'#FF9090'} /> ) } <SelfCircle /> <CenterControl onClick={() => { if (geoPermission === 'granted') { // load from cache to avoid unintentional re-rending // becoz geolocation is updated frequently setCenter(checkPosition(JSON.parse(localStorage.getItem('geolocation')))) } else if ( geoPermission !== 'denied' ) { // ask for loading geolocation navigator.geolocation.getCurrentPosition(({coords: {latitude, longitude}}) => { setGeolocation( {lat: latitude, lng: longitude} ) setCenter( {lat: latitude, lng: longitude} ) setGeoPermission('granted') }) } }} /> </MapContainer> </Box> ) } export default RouteMap // HK location if no valid value const checkPosition = (position) => { if ( position && typeof position.lat === 'number' && isFinite(position.lat) && typeof position.lng === 'number' && isFinite(position.lng) ) return position return {lat: 22.302711, lng: 114.177216} } const getPoint = ({lat, lng}) => [lat, lng] const BusStopMarker = ( {active, passed} ) => { const classes = useStyles() return ( <img src="https://unpkg.com/[email protected]/dist/images/marker-icon.png" alt="" tabIndex="0" className={`${classes.marker} ${active ? classes.active : ''}`} style={{ filter: passed ? 'grayscale(100%)' : 'hue-rotate(130deg)', }} /> ) } const useStyles = makeStyles ( theme => ({ mapContainer: { height: '30vh' }, centerControl: { padding: '5px', }, centerControlContainer: { background: 'white', height: '28px', marginBottom: '20px !important', marginRight: '5px !important' }, marker: { marginLeft: '38px', marginTop: '-14px', width: '25px', height: '41px', zIndex: 618, outline: 'none' }, active: { animation: '$blinker 2s linear infinite' }, "@keyframes blinker": { '50%': { opacity: 0.3 } } }) )