src/applications/static-pages/facilities/vet-center/NearByVALocations.jsx
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { connect, useDispatch } from 'react-redux';
import { multiTypeQuery } from '../actions';
import {
calculateBoundingBox,
convertMetersToMiles,
distancesToNearbyVetCenters,
} from '../../../facility-locator/utils/facilityDistance';
import { getFeaturesFromAddress } from '../../../facility-locator/utils/mapbox';
import buildFacility from './buildFacility';
import {
hasAnyMultiData,
isFinishedLoading,
isStartedLoading,
joinMultiData,
} from './multiLoadingDataHelpers';
import VAFacility from './components/VAFAcility';
const NEARBY_VA_LOCATIONS_RADIUS_MILES = 120;
const genQuery = (boundingBox, coordinates, type, mobileFalse) => {
const params = {
page: 1,
// eslint-disable-next-line camelcase
per_page: 2,
type,
radius: NEARBY_VA_LOCATIONS_RADIUS_MILES,
lat: coordinates[1],
long: coordinates[0],
bbox: boundingBox,
};
if (mobileFalse) {
params.mobile = false;
}
return params;
};
const NearbyLocations = props => {
const [originalCoordinates, setOriginalCoordinates] = useState([]);
const [nearbyVADistances, setNearbyVADistances] = useState(false);
const dispatch = useDispatch();
const fetchNearbyVALocations = useCallback(
async () => {
if (hasAnyMultiData(props)) {
return;
}
const { mainAddress } = props;
if (!mainAddress) {
return;
}
const addressQuery = `${mainAddress.addressLine1}, ${
mainAddress.locality
} ${mainAddress.administrativeArea} ${mainAddress.postalCode}`;
const mapboxResponse = await getFeaturesFromAddress(addressQuery);
const coordinates = mapboxResponse?.body.features[0].center; // [longitude,latitude]
if (!coordinates) {
return;
}
setOriginalCoordinates(coordinates);
const boundingBox = calculateBoundingBox(
coordinates[1],
coordinates[0],
NEARBY_VA_LOCATIONS_RADIUS_MILES,
);
dispatch(
multiTypeQuery(
'Health',
'/va',
genQuery(boundingBox, coordinates, 'health', true),
),
);
dispatch(
multiTypeQuery(
'Cemetery',
'/va',
genQuery(boundingBox, coordinates, 'cemetery', false),
),
);
dispatch(
multiTypeQuery(
'VetCenter',
'/va',
genQuery(boundingBox, coordinates, 'vet_center', true),
),
);
},
[props, dispatch],
);
useEffect(
() => {
if (!props.togglesLoading && !isStartedLoading(props.multiLoading)) {
fetchNearbyVALocations();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.togglesLoading, props.multiLoading],
);
useEffect(
() => {
const noDistancesToMeasure =
originalCoordinates.length === 0 || !isFinishedLoading(props);
if (nearbyVADistances || noDistancesToMeasure) {
return false;
}
const facilityCoordinates = joinMultiData(props)
.filter(center => center.id !== props.mainFacilityApiId)
.map(center => ({
id: center.id,
coordinates: [center.attributes.long, center.attributes.lat],
}));
const fetchDrivingData = async () => {
if (nearbyVADistances) {
return;
}
const response = await fetch(
distancesToNearbyVetCenters(
originalCoordinates,
facilityCoordinates.map(center => center.coordinates),
),
);
const data = await response.json();
const nearbyDistances = data.distances.map(distance =>
convertMetersToMiles(distance[0]),
);
const facilityCoordinatesWithDistances = facilityCoordinates.map(
(center, index) => ({
...center,
distance: nearbyDistances[index],
}),
);
setNearbyVADistances(facilityCoordinatesWithDistances);
};
fetchDrivingData();
return false;
},
[props, originalCoordinates, nearbyVADistances],
);
const normalizeFetchedFacilityProperties = useCallback(
vc => {
let centerDistance = false;
if (isFinishedLoading(props) && nearbyVADistances.length) {
const facilityDistance = nearbyVADistances.find(
distance => distance.id === vc.id,
);
centerDistance = facilityDistance.distance;
}
return buildFacility(vc, centerDistance);
},
[nearbyVADistances, props],
);
const normalizeFetchedFacilities = vcs => {
return (
vcs
.map(vc => normalizeFetchedFacilityProperties(vc))
.sort((a, b) => a.distance - b.distance)
// pulls out one of each Health, VetCenter, and Cemetery facilityType from the multidata that has been
// joined together in order to process distances in one array.
.reduce(
(acc, vaf) => {
if (vaf.source === 'Health' && acc[0] === null) {
acc[0] = vaf;
} else if (vaf.source === 'VetCenter' && acc[1] === null) {
acc[1] = vaf;
} else if (vaf.source === 'Cemetery' && acc[2] === null) {
acc[2] = vaf;
}
return acc;
},
[null, null, null],
)
// Since it may be that one of the requests to the API returned data with an empty list (i.e. no Cemetery within 120 miles)
// the above array of 3 elements may have a null since the array uses index for the type of facility and the order matters.
.filter(v => v)
);
};
const renderNearbyFacilitiesContainer = sortedVaLocations => {
// Filter here so we can choose to use the sorted list if there are no Vet centers within the birds-eye radius
const filteredByDistance = sortedVaLocations.filter(
vc => vc.distance < NEARBY_VA_LOCATIONS_RADIUS_MILES,
);
// Distance is calculated using the driving distance not birds-eye distance so all results may be outside the radius
const useSorted = filteredByDistance.length === 0;
return (
<div>
{(useSorted ? sortedVaLocations : filteredByDistance).map(vf => (
<VAFacility key={vf.id} vaFacility={vf} mainPhone={props.mainPhone} />
))}
</div>
);
};
// Possible returns with the components
if (isStartedLoading(props.multiLoading)) {
return <va-loading-indicator message="Loading facilities..." />;
}
const joined = joinMultiData(props);
if (joined.length > 0) {
// only render the section if there are some facilities within the birds-eye radius
const normalizedFetchedFacilities = normalizeFetchedFacilities(joined);
return renderNearbyFacilitiesContainer(normalizedFetchedFacilities);
}
return (
<div>
<p>No nearby VA locations found.</p>
</div>
);
};
NearbyLocations.propTypes = {
mainAddress: PropTypes.object,
mainFacilityApiId: PropTypes.string,
mainPhone: PropTypes.string,
multiLoading: PropTypes.objectOf(PropTypes.bool),
multidata: PropTypes.objectOf(PropTypes.object),
nearbyLocations: PropTypes.array,
togglesLoading: PropTypes.bool,
};
const mapStateToProps = store => ({
multiLoading: store.facility?.multiLoading,
multidata: store.facility?.multidata,
togglesLoading: store.featureToggles?.loading,
});
export default connect(mapStateToProps)(NearbyLocations);