From 962ee4cf02ca6469c7a0b9b17697ffdd0171cc0b Mon Sep 17 00:00:00 2001 From: Kelly Innes Date: Mon, 26 Aug 2019 12:13:11 -0400 Subject: [PATCH 1/3] Use selected marker for selected facility When a facility has been selected -- via URL change, which can be triggered by marker clicks -- set the facility marker in the vector tile layer to use the selected marker. --- .../components/VectorTileFacilitiesLayer.jsx | 76 ++++++++++++++----- .../components/VectorTileFacilitiesMap.jsx | 34 ++++++++- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/app/src/components/VectorTileFacilitiesLayer.jsx b/src/app/src/components/VectorTileFacilitiesLayer.jsx index 5e2006f1f..822dac2e9 100644 --- a/src/app/src/components/VectorTileFacilitiesLayer.jsx +++ b/src/app/src/components/VectorTileFacilitiesLayer.jsx @@ -1,15 +1,25 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { bool, func, number, string } from 'prop-types'; import { connect } from 'react-redux'; import VectorGridDefault from 'react-leaflet-vectorgrid'; import { withLeaflet } from 'react-leaflet'; import L from 'leaflet'; import isEmpty from 'lodash/isEmpty'; +import get from 'lodash/get'; import { createQueryStringFromSearchFilters } from '../util/util'; const VectorGrid = withLeaflet(VectorGridDefault); +const createMarkerIcon = iconUrl => L.icon({ + iconUrl, + iconSize: [30, 40], + iconAnchor: [15, 40], +}); + +const selectedMarkerIcon = createMarkerIcon('/images/selectedmarker.png'); +const unselectedMarkerIcon = createMarkerIcon('/images/marker.png'); + function useUpdateTileURL(tileURL, performingNewSearch, resetButtonClickCount) { const [ vectorTileURLWithQueryParams, @@ -46,13 +56,41 @@ function useUpdateTileURL(tileURL, performingNewSearch, resetButtonClickCount) { return vectorTileURLWithQueryParams; } +const useUpdateTileLayerWithMarkerForSelectedOARID = (oarID) => { + const tileLayerRef = useRef(null); + + const [currentSelectedMarkerID, setCurrentSelectedMarkerID] = useState(oarID); + + useEffect(() => { + if (tileLayerRef && (oarID !== currentSelectedMarkerID)) { + const tileLayer = get( + tileLayerRef, + 'current.leafletElement', + ); + + tileLayer.setFeatureStyle(currentSelectedMarkerID, { + icon: unselectedMarkerIcon, + }); + + tileLayer.setFeatureStyle(oarID, { + icon: selectedMarkerIcon, + }); + + setCurrentSelectedMarkerID(oarID); + } + }, [oarID, currentSelectedMarkerID, setCurrentSelectedMarkerID, tileLayerRef]); + + return tileLayerRef; +}; + const VectorTileFacilitiesLayer = ({ tileURL, - handleClick, + handleMarkerClick, fetching, resetButtonClickCount, tileCacheKey, getNewCacheKey, + oarID, }) => { const vectorTileURL = useUpdateTileURL( tileURL, @@ -61,6 +99,8 @@ const VectorTileFacilitiesLayer = ({ getNewCacheKey, ); + const vectorTileLayerRef = useUpdateTileLayerWithMarkerForSelectedOARID(oarID); + if (!tileCacheKey) { // We throw an error here if the tile cache key is missing. // This crashes the map, intentionally, but an ErrorBoundary @@ -70,33 +110,42 @@ const VectorTileFacilitiesLayer = ({ return ( get(f, 'properties.id', null)} /> ); }; +VectorTileFacilitiesLayer.defaultProps = { + oarID: null, +}; + VectorTileFacilitiesLayer.propTypes = { - handleClick: func.isRequired, + handleMarkerClick: func.isRequired, tileURL: string.isRequired, tileCacheKey: string.isRequired, fetching: bool.isRequired, resetButtonClickCount: number.isRequired, + oarID: string, }; const createURLWithQueryString = (qs, key) => @@ -127,13 +176,6 @@ function mapStateToProps({ }; } -function mapDispatchToProps() { - return { - handleClick: ({ layer }) => window.console.log(layer), - }; -} - export default connect( mapStateToProps, - mapDispatchToProps, )(VectorTileFacilitiesLayer); diff --git a/src/app/src/components/VectorTileFacilitiesMap.jsx b/src/app/src/components/VectorTileFacilitiesMap.jsx index 8929a95a1..055ff9251 100644 --- a/src/app/src/components/VectorTileFacilitiesMap.jsx +++ b/src/app/src/components/VectorTileFacilitiesMap.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { bool, number, string } from 'prop-types'; +import { bool, func, number, shape, string } from 'prop-types'; import { connect } from 'react-redux'; import { Map as ReactLeafletMap, ZoomControl } from 'react-leaflet'; import ReactLeafletGoogleLayer from 'react-leaflet-google-layer'; @@ -15,6 +15,8 @@ import VectorTileFacilitiesLayer from './VectorTileFacilitiesLayer'; import { COUNTRY_CODES } from '../util/constants'; +import { makeFacilityDetailLink } from '../util/util'; + import { initialCenter, initialZoom, @@ -65,6 +67,10 @@ function VectorTileFacilitiesMap({ resetButtonClickCount, clientInfoFetched, countryCode, + handleMarkerClick, + match: { + params: { oarID }, + }, }) { const mapRef = useUpdateLeafletMapImperatively(resetButtonClickCount); @@ -108,7 +114,10 @@ function VectorTileFacilitiesMap({ - + ); } @@ -117,6 +126,12 @@ VectorTileFacilitiesMap.propTypes = { resetButtonClickCount: number.isRequired, clientInfoFetched: bool.isRequired, countryCode: string.isRequired, + handleMarkerClick: func.isRequired, + match: shape({ + params: shape({ + oarID: string, + }), + }).isRequired, }; function mapStateToProps({ @@ -132,4 +147,17 @@ function mapStateToProps({ }; } -export default connect(mapStateToProps)(VectorTileFacilitiesMap); +function mapDispatchToProps(_, { history: { push } }) { + return { + handleMarkerClick: (e) => { + const oarID = get(e, 'layer.properties.id', null); + + return oarID ? push(makeFacilityDetailLink(oarID)) : noop(); + }, + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(VectorTileFacilitiesMap); From 50f4092d1c93f55376f0b66b6d9c7c4028786c31 Mon Sep 17 00:00:00 2001 From: Kelly Innes Date: Mon, 26 Aug 2019 15:06:43 -0400 Subject: [PATCH 2/3] Set map view on facility contextually When the user has arrived at the map by visiting a URL which contains an OAR ID, set the map view to center on the facility when the facility data returns. Otherwise, selecting a facility will not re-center the viewport. If the OAR ID encoded in a URL returns an error, switch off the setView behavior to ensure that the next manually selected facility does not incite a setView call. If a facility which is off-screen is selected (as may happen when selecting a facility from the list sidebar), center the map view on the facility when its data returns. --- .../components/VectorTileFacilitiesMap.jsx | 120 +++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/src/app/src/components/VectorTileFacilitiesMap.jsx b/src/app/src/components/VectorTileFacilitiesMap.jsx index 055ff9251..a390cbcf4 100644 --- a/src/app/src/components/VectorTileFacilitiesMap.jsx +++ b/src/app/src/components/VectorTileFacilitiesMap.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { bool, func, number, shape, string } from 'prop-types'; +import { arrayOf, bool, func, number, shape, string } from 'prop-types'; import { connect } from 'react-redux'; import { Map as ReactLeafletMap, ZoomControl } from 'react-leaflet'; import ReactLeafletGoogleLayer from 'react-leaflet-google-layer'; @@ -9,6 +9,9 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import { toast } from 'react-toastify'; import noop from 'lodash/noop'; import get from 'lodash/get'; +import head from 'lodash/head'; +import last from 'lodash/last'; +import delay from 'lodash/delay'; import Button from './Button'; import VectorTileFacilitiesLayer from './VectorTileFacilitiesLayer'; @@ -17,9 +20,12 @@ import { COUNTRY_CODES } from '../util/constants'; import { makeFacilityDetailLink } from '../util/util'; +import { facilityDetailsPropType } from '../util/propTypes'; + import { initialCenter, initialZoom, + detailsZoomLevel, GOOGLE_CLIENT_SIDE_API_KEY, } from '../util/constants.facilitiesMap'; @@ -35,9 +41,95 @@ const mapComponentStyles = Object.freeze({ }), }); -function useUpdateLeafletMapImperatively(resetButtonClickCount) { +function useUpdateLeafletMapImperatively( + resetButtonClickCount, + { oarID, data, error, fetching }, +) { const mapRef = useRef(null); + // Set the map view on a facility location if the user has arrived + // directly from a URL containing a valid OAR ID + const [ + shouldSetViewOnReceivingData, + setShouldSetViewOnReceivingData, + ] = useState(!!oarID); + + useEffect(() => { + if (shouldSetViewOnReceivingData) { + if (data) { + const leafletMap = get(mapRef, 'current.leafletElement', null); + + const facilityLocation = get( + data, + 'geometry.coordinates', + null, + ); + + if (leafletMap && facilityLocation) { + leafletMap.setView( + { + lng: head(facilityLocation), + lat: last(facilityLocation), + }, + detailsZoomLevel, + ); + } + + setShouldSetViewOnReceivingData(false); + } else if (error) { + setShouldSetViewOnReceivingData(false); + } + } + }, [ + shouldSetViewOnReceivingData, + setShouldSetViewOnReceivingData, + data, + error, + ]); + + // Set the map view on the facility location if it is not within the + // current viewport bbox + const [appIsGettingFacilityData, setAppIsGettingFacilityData] = useState(fetching); + + useEffect(() => { + if (shouldSetViewOnReceivingData) { + noop(); + } else if (fetching && !appIsGettingFacilityData) { + setAppIsGettingFacilityData(true); + } else if (!fetching && appIsGettingFacilityData && data) { + const leafletMap = get(mapRef, 'current.leafletElement', null); + const facilityLocation = get(data, 'geometry.coordinates', null); + + delay( + () => { + if (leafletMap && facilityLocation) { + const facilityLatLng = { + lng: head(facilityLocation), + lat: last(facilityLocation), + }; + + const mapBoundsContainsFacility = leafletMap + .getBounds() + .contains(facilityLatLng); + + if (!mapBoundsContainsFacility) { + leafletMap.setView(facilityLatLng); + } + } + + setAppIsGettingFacilityData(false); + }, + 0, + ); + } + }, [ + fetching, + appIsGettingFacilityData, + setAppIsGettingFacilityData, + data, + shouldSetViewOnReceivingData, + ]); + // Reset the map state when the reset button is clicked const [ currentResetButtonClickCount, @@ -71,8 +163,16 @@ function VectorTileFacilitiesMap({ match: { params: { oarID }, }, + facilityDetailsData, + errorFetchingFacilityDetailsData, + fetchingDetailsData, }) { - const mapRef = useUpdateLeafletMapImperatively(resetButtonClickCount); + const mapRef = useUpdateLeafletMapImperatively(resetButtonClickCount, { + oarID, + data: facilityDetailsData, + fetching: fetchingDetailsData, + error: errorFetchingFacilityDetailsData, + }); if (!clientInfoFetched) { return null; @@ -122,6 +222,11 @@ function VectorTileFacilitiesMap({ ); } +VectorTileFacilitiesMap.defaultProps = { + facilityDetailsData: null, + errorFetchingFacilityDetailsData: null, +}; + VectorTileFacilitiesMap.propTypes = { resetButtonClickCount: number.isRequired, clientInfoFetched: bool.isRequired, @@ -132,6 +237,9 @@ VectorTileFacilitiesMap.propTypes = { oarID: string, }), }).isRequired, + facilityDetailsData: facilityDetailsPropType, + errorFetchingFacilityDetailsData: arrayOf(string), + fetchingDetailsData: bool.isRequired, }; function mapStateToProps({ @@ -139,11 +247,17 @@ function mapStateToProps({ facilitiesSidebarTabSearch: { resetButtonClickCount }, }, clientInfo: { fetched, countryCode }, + facilities: { + singleFacility: { data, error, fetching }, + }, }) { return { resetButtonClickCount, clientInfoFetched: fetched, countryCode: countryCode || COUNTRY_CODES.default, + facilityDetailsData: data, + errorFetchingFacilityDetailsData: error, + fetchingDetailsData: fetching, }; } From 2a870400ff8cdccd5d5212092b3cf84cd6e083ee Mon Sep 17 00:00:00 2001 From: Kelly Innes Date: Mon, 26 Aug 2019 15:17:08 -0400 Subject: [PATCH 3/3] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089e25218..12e005ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- Adjust marker icon on selecting a new facility on the vector tiles layer [#749](https://github.com/open-apparel-registry/open-apparel-registry/pull/749) ### Changed