diff --git a/components/aoi-card/component.jsx b/components/aoi-card/component.jsx index 45ea86cf0a..1a5a9b66ea 100644 --- a/components/aoi-card/component.jsx +++ b/components/aoi-card/component.jsx @@ -52,6 +52,7 @@ const getLatestAlerts = ({ location, params }) => class AoICard extends PureComponent { static propTypes = { + admin: PropTypes.object, name: PropTypes.string, tags: PropTypes.array, application: PropTypes.string, @@ -64,6 +65,7 @@ class AoICard extends PureComponent { onFetchAlerts: PropTypes.func, status: PropTypes.string, setConfirmSubscriptionModalSettings: PropTypes.func, + openOutdatedAreaModal: PropTypes.func, confirmed: PropTypes.bool, id: PropTypes.string, }; @@ -131,6 +133,7 @@ class AoICard extends PureComponent { render() { const { + admin, tags, name, application, @@ -142,9 +145,13 @@ class AoICard extends PureComponent { location, status, setConfirmSubscriptionModalSettings, + openOutdatedAreaModal, id, confirmed, } = this.props; + + const isOutdatedGadmArea = + admin?.source?.provider === 'gadm' && admin?.source?.version === '3.6'; const { loading, alerts: { glads, fires, error: dataError }, @@ -237,6 +244,20 @@ class AoICard extends PureComponent { )} )} + {isOutdatedGadmArea && ( +
+ + +
+ )} {!simple && !isPending && (
diff --git a/components/forms/area-of-interest/actions.js b/components/forms/area-of-interest/actions.js index 11303eb61c..62b7a7132a 100644 --- a/components/forms/area-of-interest/actions.js +++ b/components/forms/area-of-interest/actions.js @@ -67,7 +67,7 @@ export const saveAreaOfInterest = createThunkAction( admin: { source: { provider: 'gadm', - version: '3.6', + version: '4.1', }, adm0, adm1, diff --git a/components/map-menu/actions.js b/components/map-menu/actions.js index 9bf81a89c0..c26a5d8b10 100644 --- a/components/map-menu/actions.js +++ b/components/map-menu/actions.js @@ -1,7 +1,7 @@ import { createAction, createThunkAction } from 'redux/actions'; import { fetchGeocodeLocations } from 'services/geocoding'; -import { setMapSettings, setMapInteractions } from 'components/map/actions'; +import { setMapSettings } from 'components/map/actions'; import { setAnalysisSettings } from 'components/analysis/actions'; export const setLocationsData = createAction('setLocationsData'); @@ -10,58 +10,64 @@ export const setMenuSettings = createAction('setMenuSettings'); export const getLocationFromSearch = createThunkAction( 'getLocationFromSearch', - ({ search, token, lang }) => (dispatch) => { - dispatch(setMenuLoading(true)); - if (search) { - fetchGeocodeLocations(search, lang, token) - .then((locations) => { - if (locations?.length) { - dispatch(setLocationsData(locations)); - } else { - dispatch(setLocationsData([])); - } - dispatch(setMenuLoading(false)); - }) - .catch(() => { - dispatch(setMenuLoading(false)); - }); + ({ search, token, lang }) => + (dispatch) => { + dispatch(setMenuLoading(true)); + if (search) { + fetchGeocodeLocations(search, lang, token) + .then((locations) => { + if (locations?.length) { + dispatch(setLocationsData(locations)); + } else { + dispatch(setLocationsData([])); + } + dispatch(setMenuLoading(false)); + }) + .catch(() => { + dispatch(setMenuLoading(false)); + }); + } } - } ); export const handleClickLocation = createThunkAction( 'handleClickLocation', - ({ center, bbox: featureBbox, ...feature }) => (dispatch) => { - if (featureBbox) { - dispatch(setMapSettings({ canBound: true, bbox: featureBbox })); - } else { - dispatch( - setMapSettings({ center: { lat: center[1], lng: center[0] }, zoom: 12 }) - ); + ({ center, bbox: featureBbox }) => + (dispatch) => { + if (featureBbox) { + dispatch(setMapSettings({ canBound: true, bbox: featureBbox })); + } else { + dispatch( + setMapSettings({ + center: { lat: center[1], lng: center[0] }, + zoom: 12, + }) + ); + } + + dispatch(setMenuSettings({ menuSection: '' })); } - dispatch(setMapInteractions({ features: [feature], lngLat: center })); - dispatch(setMenuSettings({ menuSection: '' })); - } ); export const handleViewOnMap = createThunkAction( 'handleViewOnMap', - ({ analysis, mapMenu, map }) => (dispatch) => { - if (map) { - dispatch(setMapSettings({ ...map, canBound: true })); - } + ({ analysis, mapMenu, map }) => + (dispatch) => { + if (map) { + dispatch(setMapSettings({ ...map, canBound: true })); + } - dispatch( - setMenuSettings({ - ...mapMenu, - menuSection: '', - }) - ); + dispatch( + setMenuSettings({ + ...mapMenu, + menuSection: '', + }) + ); - if (analysis) { - dispatch(setAnalysisSettings(analysis)); + if (analysis) { + dispatch(setAnalysisSettings(analysis)); + } } - } ); export const showAnalysis = createThunkAction( diff --git a/components/map-menu/components/sections/my-gfw/component.jsx b/components/map-menu/components/sections/my-gfw/component.jsx index 9c4a4ddf33..8950434c02 100644 --- a/components/map-menu/components/sections/my-gfw/component.jsx +++ b/components/map-menu/components/sections/my-gfw/component.jsx @@ -17,6 +17,7 @@ import Pill from 'components/ui/pill'; import Loader from 'components/ui/loader'; import Paginate from 'components/paginate'; import ConfirmSubscriptionModal from 'components/modals/confirm-subscription'; +import OutdatedAreaModal from 'components/modals/outdated-area/OutdatedAreaModal'; import editIcon from 'assets/icons/edit.svg?sprite'; import shareIcon from 'assets/icons/share.svg?sprite'; @@ -51,6 +52,7 @@ class MapMenuMyGFW extends PureComponent { unselectedTags: [], pageSize: 6, pageNum: 0, + outdatedAreaModalOpen: false, }; static getDerivedStateFromProps(prevProps, prevState) { @@ -294,7 +296,15 @@ class MapMenuMyGFW extends PureComponent { tabIndex={0} key={area.id} > - + + this.setState({ + outdatedAreaModalOpen: true, + })} + /> {active && this.renderAoiActions()}
); @@ -370,6 +380,13 @@ class MapMenuMyGFW extends PureComponent { /> )} + + this.setState({ + outdatedAreaModalOpen: false, + })} + /> ); } diff --git a/components/map-menu/components/sections/search/selectors.js b/components/map-menu/components/sections/search/selectors.js index ad2372c322..0824913882 100644 --- a/components/map-menu/components/sections/search/selectors.js +++ b/components/map-menu/components/sections/search/selectors.js @@ -1,6 +1,6 @@ import { createSelector, createStructuredSelector } from 'reselect'; import { deburrUpper } from 'utils/strings'; -import { getGadm36Id } from 'utils/gadm'; +import { getGadmId } from 'utils/gadm'; import sortBy from 'lodash/sortBy'; import { translateText, selectActiveLang } from 'utils/lang'; @@ -49,7 +49,7 @@ const getLocations = createSelector( (locations, location) => { if (!locations) return null; const { adm0, adm1, adm2 } = location; - const gadmId = getGadm36Id(adm0, adm1, adm2); + const gadmId = getGadmId(adm0, adm1, adm2); return locations .map((l) => ({ diff --git a/components/map/components/popup/components/boundary-sentence/selectors.js b/components/map/components/popup/components/boundary-sentence/selectors.js index c4305db53a..2588091f91 100644 --- a/components/map/components/popup/components/boundary-sentence/selectors.js +++ b/components/map/components/popup/components/boundary-sentence/selectors.js @@ -6,7 +6,7 @@ const getInteractionData = (state, { data }) => data; /** * Returns an object with the selected location name, its area and a sentence do be displayed. - * @param {method} createSelector - return a memoized outut selector. + * @param {method} createSelector - return a memoized output selector. * @see https://reselect.js.org/introduction/getting-started/#output-selector for implementation details * @param {selector} getInteractionData - data from the area clicked by user * @return {object} sentence, location name and area. @@ -14,8 +14,8 @@ const getInteractionData = (state, { data }) => data; export const getSentence = createSelector( [getInteractionData], ({ data } = {}) => { - const { adm_level, gid_0, name_1, country } = data; - let name = adm_level > 0 ? data[`name_${adm_level}`] : country; + const { adm_level, gid_0, name_1, name_0, country } = data; + let name = adm_level > 0 ? data[`name_${adm_level}`] : country || name_0; if (!gid_0) { name = data[Object.keys(data).find((k) => k.includes('name'))]; @@ -30,12 +30,15 @@ export const getSentence = createSelector( locationNames = [ locationNameTranslated, translateText(name_1), - translateText(country), + translateText(country || name_0), ]; } - if (Number(adm_level) === '1') { - locationNames = [locationNameTranslated, translateText(country)]; + if (Number(adm_level) === 1) { + locationNames = [ + locationNameTranslated, + translateText(country || name_0), + ]; } const locationName = locationNames.join(', '); diff --git a/components/map/selectors.js b/components/map/selectors.js index 95e3d336be..0642032118 100644 --- a/components/map/selectors.js +++ b/components/map/selectors.js @@ -48,7 +48,9 @@ export const getMapViewport = createSelector([getMapSettings], (settings) => { pitch, latitude: center?.lat, longitude: center?.lng, - transitionDuration: 500, + // The map transition needs to always be 0 otherwise the map becomes sluggish when panned or zoomed. Only set a + // different value when flying between locations and only temporarily. + transitionDuration: 0, }; }); diff --git a/components/modals/outdated-area/OutdatedAreaModal.js b/components/modals/outdated-area/OutdatedAreaModal.js new file mode 100644 index 0000000000..4c0aed9020 --- /dev/null +++ b/components/modals/outdated-area/OutdatedAreaModal.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'components/modal'; + +const OutdatedAreaModal = ({ isOpen, handleCloseModal }) => { + return ( + +

+ This area uses an outdated version of political boundaries. As a result, + alert notification emails can no longer be sent for this area. To + continue receiving alerts, please navigate to this area on the map and + re-save the area. Instructions on how to save an area and subscribe to + alerts are available via in{' '} + + this Help Center article + + . Please read through our{' '} + + blog outlining these changes + {' '} + and let us know if you have any questions at{' '} + gfw@wri.org. +

+
+ ); +}; + +OutdatedAreaModal.propTypes = { + isOpen: PropTypes.bool, + handleCloseModal: PropTypes.func, +}; + +export default OutdatedAreaModal; diff --git a/components/ui/map/component.jsx b/components/ui/map/component.jsx index 5012fae3cf..df2c8e8747 100644 --- a/components/ui/map/component.jsx +++ b/components/ui/map/component.jsx @@ -271,6 +271,9 @@ class Map extends Component { onResize={this.onResize} onLoad={this.onLoad} getCursor={getCursor} + // If the `transitionDuration` is not 0, then the map becomes sluggish when panned or zoomed. Nevertheless, + // we still want a transition when flying between locations. + transitionDuration={flying ? viewport.transitionDuration || 0 : 0} transitionInterpolator={new FlyToInterpolator()} transitionEasing={easeCubic} preventStyleDiffing diff --git a/data/analysis-datasets-versions.json b/data/analysis-datasets-versions.json index 140ec84655..d781d9ccb4 100644 --- a/data/analysis-datasets-versions.json +++ b/data/analysis-datasets-versions.json @@ -1,13 +1,13 @@ { - "ANNUAL_ADM0_CHANGE": "v20240815", - "ANNUAL_ADM0_SUMMARY": "v20240815", - "ANNUAL_ADM0_WHITELIST": "v20240815", - "ANNUAL_ADM1_CHANGE": "v20240815", - "ANNUAL_ADM1_SUMMARY": "v20240815", - "ANNUAL_ADM1_WHITELIST": "v20240815", - "ANNUAL_ADM2_CHANGE": "v20240815", - "ANNUAL_ADM2_SUMMARY": "v20240815", - "ANNUAL_ADM2_WHITELIST": "v20240815", + "ANNUAL_ADM0_CHANGE": "v20250122", + "ANNUAL_ADM0_SUMMARY": "v20250122", + "ANNUAL_ADM0_WHITELIST": "v20250122", + "ANNUAL_ADM1_CHANGE": "v20250122", + "ANNUAL_ADM1_SUMMARY": "v20250122", + "ANNUAL_ADM1_WHITELIST": "v20250122", + "ANNUAL_ADM2_CHANGE": "v20250122", + "ANNUAL_ADM2_SUMMARY": "v20250122", + "ANNUAL_ADM2_WHITELIST": "v20250122", "ANNUAL_GEOSTORE_CHANGE": "v20241209", "ANNUAL_GEOSTORE_SUMMARY": "v20241209", @@ -16,13 +16,13 @@ "ANNUAL_WDPA_SUMMARY": "v20240813", "ANNUAL_WDPA_WHITELIST": "v20240813", - "VIIRS_ADM0_WHITELIST": "v20240815", - "VIIRS_ADM1_WHITELIST": "v20240815", - "VIIRS_ADM2_WHITELIST": "v20240815", - "VIIRS_ADM2_DAILY": "v20240815", - "VIIRS_ADM0_WEEKLY": "v20240815", - "VIIRS_ADM1_WEEKLY": "v20240815", - "VIIRS_ADM2_WEEKLY": "v20240815", + "VIIRS_ADM0_WHITELIST": "v20250206", + "VIIRS_ADM1_WHITELIST": "v20250206", + "VIIRS_ADM2_WHITELIST": "v20250206", + "VIIRS_ADM2_DAILY": "v20250206", + "VIIRS_ADM0_WEEKLY": "v20250206", + "VIIRS_ADM1_WEEKLY": "v20250206", + "VIIRS_ADM2_WEEKLY": "v20250206", "VIIRS_GEOSTORE_DAILY": "v20241209", "VIIRS_GEOSTORE_WEEKLY": "v20241209", @@ -31,13 +31,13 @@ "VIIRS_WDPA_WEEKLY": "v20240122", "VIIRS_WDPA_WHITELIST": "v20240122", - "MODIS_ADM0_WHITELIST": "v20240815", - "MODIS_ADM1_WHITELIST": "v20240815", - "MODIS_ADM2_WHITELIST": "v20240815", - "MODIS_ADM2_DAILY": "v20240815", - "MODIS_ADM0_WEEKLY": "v20240815", - "MODIS_ADM1_WEEKLY": "v20240815", - "MODIS_ADM2_WEEKLY": "v20240815", + "MODIS_ADM0_WHITELIST": "v20250206", + "MODIS_ADM1_WHITELIST": "v20250206", + "MODIS_ADM2_WHITELIST": "v20250206", + "MODIS_ADM2_DAILY": "v20250206", + "MODIS_ADM0_WEEKLY": "v20250206", + "MODIS_ADM1_WEEKLY": "v20250206", + "MODIS_ADM2_WEEKLY": "v20250206", "MODIS_GEOSTORE_WHITELIST": "v20240122", "MODIS_GEOSTORE_DAILY": "v20240122", diff --git a/layouts/dashboards/components/header/selectors.js b/layouts/dashboards/components/header/selectors.js index 8d16e6d8e4..5e1d59c75a 100644 --- a/layouts/dashboards/components/header/selectors.js +++ b/layouts/dashboards/components/header/selectors.js @@ -84,7 +84,14 @@ export const getFirstUserArea = createSelector([getUserAreas], (areas) => export const getAdm0Data = createSelector( [getAdminMetadata], - (data) => data && data.adm0 + (data) => + data && + data.adm0.sort((a, b) => + a.label.localeCompare(b.label, 'en', { + sensitivity: 'base', + ignorePunctuation: true, + }) + ) ); export const getAdm1Data = createSelector( diff --git a/layouts/my-gfw/components/areas-table/component.jsx b/layouts/my-gfw/components/areas-table/component.jsx index 37d603e09d..46e6034f1f 100644 --- a/layouts/my-gfw/components/areas-table/component.jsx +++ b/layouts/my-gfw/components/areas-table/component.jsx @@ -16,6 +16,7 @@ import Dropdown from 'components/ui/dropdown'; import Search from 'components/ui/search'; import Paginate from 'components/paginate'; import ConfirmSubscriptionModal from 'components/modals/confirm-subscription'; +import OutdatedAreaModal from 'components/modals/outdated-area/OutdatedAreaModal'; import mapIcon from 'assets/icons/view-map.svg?sprite'; import editIcon from 'assets/icons/edit.svg?sprite'; @@ -42,6 +43,7 @@ class AreasTable extends PureComponent { alerts: {}, pageSize: 6, pageNum: 0, + outdatedAreaModalOpen: false, }; componentDidUpdate(prevProps) { @@ -252,6 +254,10 @@ class AreasTable extends PureComponent { this.setState({ alerts: { ...allAlerts, [area.id]: alertsResponse }, })} + openOutdatedAreaModal={() => + this.setState({ + outdatedAreaModalOpen: true, + })} /> @@ -320,6 +326,13 @@ class AreasTable extends PureComponent { /> )} + + this.setState({ + outdatedAreaModalOpen: false, + })} + /> ); } diff --git a/pages/about.js b/pages/about.js index c78a162592..7c007d8806 100644 --- a/pages/about.js +++ b/pages/about.js @@ -28,7 +28,7 @@ export const getStaticProps = async () => { props: { impactProjects, sgfProjects, - countries: countries?.data?.rows, + countries: countries?.data, notifications: notifications || [], }, revalidate: 10, diff --git a/pages/dashboards/[[...location]].js b/pages/dashboards/[[...location]].js index fb817a9911..f81b632365 100644 --- a/pages/dashboards/[[...location]].js +++ b/pages/dashboards/[[...location]].js @@ -7,13 +7,12 @@ import uniqBy from 'lodash/uniqBy'; import useRouter from 'utils/router'; import { decodeQueryParams } from 'utils/url'; -import { parseGadm36Id } from 'utils/gadm'; +import { parseGadmId } from 'utils/gadm'; import { parseStringWithVars } from 'utils/strings'; import { getLocationData } from 'services/location'; import { getPublishedNotifications } from 'services/notifications'; import { - // getCountriesProvider, getRegionsProvider, getSubRegionsProvider, getCategorisedCountries, @@ -183,9 +182,9 @@ export const getServerSideProps = async ({ params, query, req }) => { const countryLinks = await getCountryLinksSerialized(); countryData = { ...countryData, - regions: uniqBy(regions.data.rows).map((row) => ({ - id: parseGadm36Id(row.id).adm1, - value: parseGadm36Id(row.id).adm1, + regions: uniqBy(regions.data).map((row) => ({ + id: parseGadmId(row.id).adm1, + value: parseGadmId(row.id).adm1, label: row.name, name: row.name, })), @@ -194,12 +193,12 @@ export const getServerSideProps = async ({ params, query, req }) => { } if (adm1) { - const subRegions = await getSubRegionsProvider(adm0, adm1); + const subRegions = await getSubRegionsProvider({ adm0, adm1 }); countryData = { ...countryData, - subRegions: uniqBy(subRegions.data.rows).map((row) => ({ - id: parseGadm36Id(row.id).adm2, - value: parseGadm36Id(row.id).adm2, + subRegions: uniqBy(subRegions.data).map((row) => ({ + id: parseGadmId(row.id).adm2, + value: parseGadmId(row.id).adm2, label: row.name, name: row.name, })), diff --git a/pages/embed/sentence/[[...location]].js b/pages/embed/sentence/[[...location]].js index 611b2d5f77..02b1c83d92 100644 --- a/pages/embed/sentence/[[...location]].js +++ b/pages/embed/sentence/[[...location]].js @@ -2,11 +2,10 @@ import { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import uniqBy from 'lodash/uniqBy'; -import { parseGadm36Id } from 'utils/gadm'; +import { parseGadmId } from 'utils/gadm'; import { getLocationData } from 'services/location'; import { - // getCountriesProvider, getRegionsProvider, getSubRegionsProvider, getCategorisedCountries, @@ -91,9 +90,9 @@ export const getServerSideProps = async ({ params }) => { const countryLinks = await getCountryLinksSerialized(); countryData = { ...countryData, - regions: uniqBy(regions.data.rows).map((row) => ({ - id: parseGadm36Id(row.id).adm1, - value: parseGadm36Id(row.id).adm1, + regions: uniqBy(regions.data).map((row) => ({ + id: parseGadmId(row.id).adm1, + value: parseGadmId(row.id).adm1, label: row.name, name: row.name, })), @@ -102,12 +101,12 @@ export const getServerSideProps = async ({ params }) => { } if (adm1) { - const subRegions = await getSubRegionsProvider(adm0, adm1); + const subRegions = await getSubRegionsProvider({ adm0, adm1 }); countryData = { ...countryData, - subRegions: uniqBy(subRegions.data.rows).map((row) => ({ - id: parseGadm36Id(row.id).adm2, - value: parseGadm36Id(row.id).adm2, + subRegions: uniqBy(subRegions.data).map((row) => ({ + id: parseGadmId(row.id).adm2, + value: parseGadmId(row.id).adm2, label: row.name, name: row.name, })), @@ -151,21 +150,6 @@ export const getServerSideProps = async ({ params }) => { }; } }; -// -// export const getStaticPaths = async () => { -// const countryData = await getCountriesProvider(); -// const { rows: countries } = countryData?.data || {}; -// const countryPaths = countries.map((c) => ({ -// params: { -// location: ['country', c.iso], -// }, -// })); -// -// return { -// paths: ['/embed/sentence/', ...countryPaths] || [], -// fallback: true, -// }; -// }; const getSentenceClientSide = async ( locationNames = null, diff --git a/pages/grants-and-fellowships/[section].js b/pages/grants-and-fellowships/[section].js index 1acd668243..0d34889f42 100644 --- a/pages/grants-and-fellowships/[section].js +++ b/pages/grants-and-fellowships/[section].js @@ -58,7 +58,7 @@ export const getServerSideProps = async ({ query }) => { title: 'Projects | Grants & Fellowships | Global Forest Watch', section: query?.section, projects: parsedProjects || [], - allCountries: allCountries?.data?.rows || [], + allCountries: allCountries?.data || [], projectCountries: uniqCountries || [], country: query?.country || '', projectsTexts: pageTexts?.[0]?.acf, diff --git a/providers/country-data-provider/actions.js b/providers/country-data-provider/actions.js index 9ec563459e..1beac13ef5 100644 --- a/providers/country-data-provider/actions.js +++ b/providers/country-data-provider/actions.js @@ -1,5 +1,5 @@ import { createAction, createThunkAction } from 'redux/actions'; -import { parseGadm36Id } from 'utils/gadm'; +import { parseGadmId } from 'utils/gadm'; import uniqBy from 'lodash/uniqBy'; import { @@ -47,10 +47,10 @@ export const getRegions = createThunkAction( getRegionsProvider(country) .then((response) => { const parsedResponse = []; - uniqBy(response.data.rows).forEach((row) => { + uniqBy(response.data).forEach((region) => { parsedResponse.push({ - id: parseGadm36Id(row.id).adm1, - name: row.name, + id: parseGadmId(region.id).adm1, + name: region.name, }); }); dispatch(setRegions(parsedResponse, 'id')); @@ -67,14 +67,13 @@ export const getSubRegions = createThunkAction( ({ adm0, adm1, token }) => (dispatch) => { dispatch(setSubRegionsLoading(true)); - getSubRegionsProvider(adm0, adm1, token) + getSubRegionsProvider({ adm0, adm1, token }) .then((subRegions) => { - const { rows } = subRegions.data; const parsedResponse = []; - uniqBy(rows).forEach((row) => { + uniqBy(subRegions?.data).forEach((subRegion) => { parsedResponse.push({ - id: parseGadm36Id(row.id).adm2, - name: row.name, + id: parseGadmId(subRegion.id).adm2, + name: subRegion.name, }); }); dispatch(setSubRegions(uniqBy(parsedResponse, 'id'))); diff --git a/providers/geostore-provider/actions.js b/providers/geostore-provider/actions.js index 2dd6c7ab30..97582bd8e4 100644 --- a/providers/geostore-provider/actions.js +++ b/providers/geostore-provider/actions.js @@ -19,7 +19,6 @@ export const fetchGeostore = createThunkAction( 'fetchGeostore', (params) => (dispatch) => { const { type, adm0, adm1, adm2, token } = params; - if (type && adm0) { dispatch(setGeostoreLoading({ loading: true, error: false })); getGeostore({ type, adm0, adm1, adm2, token }) diff --git a/services/__tests__/areas.spec.js b/services/__tests__/areas.spec.js index 4a878f2e9c..304e12646a 100644 --- a/services/__tests__/areas.spec.js +++ b/services/__tests__/areas.spec.js @@ -42,7 +42,7 @@ describe('Areas Service', () => { // assert expect(apiAuthRequest.get).toHaveBeenCalledWith( - '/v2/area?source[provider]=gadm&source[version]=3.6' + '/v2/area?source[provider]=gadm&source[version]=4.1' ); }); @@ -60,14 +60,14 @@ describe('Areas Service', () => { adm0: 'BRA', source: { provider: 'gadm', - version: '3.6', + version: '4.1', }, }, iso: { country: 'BRA', source: { provider: 'gadm', - version: '3.6', + version: '4.1', }, }, }, @@ -115,16 +115,14 @@ describe('Areas Service', () => { adm0: 'BRA', source: { provider: 'gadm', - // version: '4.1', - version: '3.6', // remove it before release gadm 4.1 + version: '4.1', }, }, iso: { country: 'BRA', source: { provider: 'gadm', - // version: '4.1', - version: '3.6', // remove it before release gadm 4.1 + version: '4.1', }, }, use: {}, diff --git a/services/__tests__/country.spec.js b/services/__tests__/country.spec.js new file mode 100644 index 0000000000..faaa929bf8 --- /dev/null +++ b/services/__tests__/country.spec.js @@ -0,0 +1,165 @@ +import { jest } from '@jest/globals'; + +import { dataRequest } from 'utils/request'; + +import { + getCountriesProvider, + getRegionsProvider, + getSubRegionsProvider, + getFAOCountriesProvider, + getCountryLinksProvider, + getCategorisedCountries, + getCountryLinksSerialized, + GADM_DATASET, +} from 'services/country'; +import countryLinks from 'services/country-links.json'; + +jest.mock('utils/request', () => ({ + dataRequest: { + get: jest.fn(), + }, +})); + +jest.mock('services/country-links.json', () => ({ + rows: [ + { + iso: 'CMR', + external_links: + '[{ "title": "Interactive Forest Atlas of Cameroon", "url": "http://cmr.forest-atlas.org/"}]', + }, + { + iso: 'CAF', + external_links: + '[{ "title": "Interactive Forest Atlas of Central African Republic", "url": "http://caf.forest-atlas.org/"}]', + }, + ], +})); + +describe('country service', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getCountriesProvider', () => { + it('should fetch countries data', async () => { + const mockResponse = { data: [{ iso: 'BRA', name: 'Brazil' }] }; + dataRequest.get.mockResolvedValue(mockResponse); + + const result = await getCountriesProvider(); + + expect(dataRequest.get).toHaveBeenCalledWith( + `${GADM_DATASET}?sql=SELECT country AS name, gid_0 AS iso FROM gadm_administrative_boundaries WHERE adm_level = '0' AND gid_0 NOT IN ('Z01', 'Z02', 'Z03', 'Z04', 'Z05', 'Z06', 'Z07', 'Z08', 'Z09', 'TWN', 'XCA') ORDER BY country` + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('getRegionsProvider', () => { + it('should fetch regions data based on adm0', async () => { + const mockResponse = { data: [{ id: 'BRA.25', name: 'São Paulo' }] }; + dataRequest.get.mockResolvedValue(mockResponse); + + const result = await getRegionsProvider({ adm0: 'BRA' }); + + expect(dataRequest.get).toHaveBeenCalledWith( + `${GADM_DATASET}?sql=SELECT name_1 AS name, gid_1 AS id FROM gadm_administrative_boundaries WHERE adm_level='1' AND gid_0 = 'BRA' ORDER BY name`, + { cancelToken: undefined } + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('getSubRegionsProvider', () => { + it('should fetch sub-regions data based on adm0 and adm1', async () => { + const mockResponse = { data: [{ id: 'BRA.25.390', name: 'Osasco' }] }; + dataRequest.get.mockResolvedValue(mockResponse); + + const result = await getSubRegionsProvider({ adm0: 'BRA', adm1: '25' }); + + expect(dataRequest.get).toHaveBeenCalledWith( + `${GADM_DATASET}?sql=SELECT gid_2 as id, name_2 as name FROM gadm_administrative_boundaries WHERE gid_0 = 'BRA' AND gid_1 = 'BRA.25_1' AND adm_level='2' AND type_2 NOT IN ('Waterbody', 'Water body', 'Water Body') ORDER BY name`, + { cancelToken: undefined } + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('getFAOCountriesProvider', () => { + it('should fetch FAO countries data', async () => { + const mockResponse = { data: [{ iso: 'BRA', name: 'BRA' }] }; + dataRequest.get.mockResolvedValue(mockResponse); + + const result = await getFAOCountriesProvider(); + + expect(dataRequest.get).toHaveBeenCalledWith( + 'dataset/fao_forest_extent/v2020/query/json?sql=SELECT iso, country AS name FROM data WHERE year = 2020' + ); + expect(result).toEqual(mockResponse); + }); + }); + + describe('getCountryLinksProvider', () => { + it('should return country links', async () => { + const result = await getCountryLinksProvider(); + + expect(result).toEqual(countryLinks); + }); + }); + + describe('getCountryLinksSerialized', () => { + it('should return serialized country links', async () => { + const result = await getCountryLinksSerialized(); + + expect(result).toEqual({ + CMR: [ + { + title: 'Interactive Forest Atlas of Cameroon', + url: 'http://cmr.forest-atlas.org/', + }, + ], + CAF: [ + { + title: 'Interactive Forest Atlas of Central African Republic', + url: 'http://caf.forest-atlas.org/', + }, + ], + }); + }); + }); + + describe('getCategorisedCountries', () => { + it('should fetch categorised countries data', async () => { + const mockGadmResponse = { data: [{ iso: 'BRA', name: 'BRA' }] }; + const mockFaoResponse = { data: [{ iso: 'BRA', name: 'BRA' }] }; + + dataRequest.get + .mockResolvedValueOnce(mockGadmResponse) + .mockResolvedValueOnce(mockFaoResponse); + + const result = await getCategorisedCountries(); + + expect(result).toEqual({ + gadmCountries: mockGadmResponse.data, + faoCountries: mockFaoResponse.data, + countries: mockGadmResponse.data, + }); + }); + + it('should return categorised countries data as options', async () => { + const mockGadmResponse = { data: [{ iso: 'BRA', name: 'BRA' }] }; + const mockFaoResponse = { data: [{ iso: 'BRA', name: 'BRA' }] }; + + dataRequest.get + .mockResolvedValueOnce(mockGadmResponse) + .mockResolvedValueOnce(mockFaoResponse); + + const result = await getCategorisedCountries(true); + + expect(result).toEqual({ + gadmCountries: [{ label: 'BRA', value: 'BRA' }], + faoCountries: [{ label: 'BRA', value: 'BRA' }], + countries: [{ label: 'BRA', value: 'BRA' }], + }); + }); + }); +}); diff --git a/services/__tests__/geostore.spec.js b/services/__tests__/geostore.spec.js index fe40565d99..6efdaf4cc6 100644 --- a/services/__tests__/geostore.spec.js +++ b/services/__tests__/geostore.spec.js @@ -55,7 +55,7 @@ describe('fetchGeostore', () => { }); expect(dataRequest.get).toHaveBeenCalledWith( - 'https://data-api.globalforestwatch.org/geostore/admin/BRA?source[provider]=gadm&source[version]=3.6&simplify=0.1', + '/geostore/admin/BRA?source[provider]=gadm&source[version]=4.1&simplify=0.1', expect.any(Object) ); }); @@ -86,7 +86,7 @@ describe('fetchGeostore', () => { }); expect(dataRequest.get).toHaveBeenCalledWith( - 'https://data-api.globalforestwatch.org/geostore/admin/BRA/25?source[provider]=gadm&source[version]=3.6&simplify=0.01', + '/geostore/admin/BRA/25?source[provider]=gadm&source[version]=4.1&simplify=0.01', expect.any(Object) ); }); diff --git a/services/__tests__/location.spec.js b/services/__tests__/location.spec.js new file mode 100644 index 0000000000..818ef35192 --- /dev/null +++ b/services/__tests__/location.spec.js @@ -0,0 +1,82 @@ +import { jest } from '@jest/globals'; + +import { + getCountriesProvider, + getRegionsProvider, + getSubRegionsProvider, +} from 'services/country'; + +import { countryConfig } from '../location'; + +jest.mock('services/country', () => ({ + getCountriesProvider: jest.fn(), + getRegionsProvider: jest.fn(), + getSubRegionsProvider: jest.fn(), +})); + +describe('countryConfig', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('adm0', () => { + it('should return the correct country data', async () => { + const mockCountries = [{ iso: 'BRA', name: 'Brazil' }]; + getCountriesProvider.mockResolvedValue({ data: mockCountries }); + + const result = await countryConfig.adm0({ adm0: 'BRA' }); + + expect(result).toEqual({ + locationName: 'Brazil', + iso: 'BRA', + }); + expect(getCountriesProvider).toHaveBeenCalledTimes(1); + }); + }); + + describe('adm1', () => { + it('should return the correct region and country data', async () => { + const mockCountries = [{ iso: 'BRA', name: 'Brazil' }]; + const mockRegions = [{ id: 'BRA.25_', name: 'São Paulo', iso: 'BRA' }]; + + getCountriesProvider.mockResolvedValue({ data: mockCountries }); + getRegionsProvider.mockResolvedValue({ data: mockRegions }); + + const result = await countryConfig.adm1({ adm0: 'BRA', adm1: '25' }); + + expect(result).toEqual({ + locationName: 'São Paulo, Brazil', + id: 'BRA.25_', + iso: 'BRA', + }); + expect(getCountriesProvider).toHaveBeenCalledTimes(1); + expect(getRegionsProvider).toHaveBeenCalledTimes(1); + }); + }); + + describe('adm2', () => { + it('should return the correct sub-region, region, and country data', async () => { + const mockCountries = [{ iso: 'BRA', name: 'Brazil' }]; + const mockRegions = [{ id: 'BRA.25_', name: 'São Paulo' }]; + const mockSubRegions = [{ id: 'BRA.25.390_', name: 'Osasco' }]; + + getCountriesProvider.mockResolvedValue({ data: mockCountries }); + getRegionsProvider.mockResolvedValue({ data: mockRegions }); + getSubRegionsProvider.mockResolvedValue({ data: mockSubRegions }); + + const result = await countryConfig.adm2({ + adm0: 'BRA', + adm1: '25', + adm2: '390', + }); + + expect(result).toEqual({ + locationName: 'Osasco, Brazil, São Paulo', + id: 'BRA.25.390_', + }); + expect(getCountriesProvider).toHaveBeenCalledTimes(1); + expect(getRegionsProvider).toHaveBeenCalledTimes(1); + expect(getSubRegionsProvider).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/services/analysis-cached.js b/services/analysis-cached.js index e686b4f39b..09059bddf5 100644 --- a/services/analysis-cached.js +++ b/services/analysis-cached.js @@ -398,7 +398,7 @@ export const getLossNaturalForest = (params) => { } const url = encodeURI( - `${requestUrl}${SQL_QUERIES.lossNaturalForest}` + `${requestUrl}${SQL_QUERIES.lossNaturalForest}&ts=${Date.now()}` .replace( /{select_location}/g, getLocationSelect({ ...params, cast: false }) @@ -1226,7 +1226,7 @@ export const getExtentNaturalForest = (params) => { } const url = encodeURI( - `${requestUrl}${SQL_QUERIES.extentNaturalForest}` + `${requestUrl}${SQL_QUERIES.extentNaturalForest}&ts=${Date.now()}` .replace( /{select_location}/g, getLocationSelect({ ...params, cast: false }) diff --git a/services/areas.js b/services/areas.js index 86ae70e8df..5bc7946b63 100644 --- a/services/areas.js +++ b/services/areas.js @@ -53,14 +53,10 @@ export const getAreas = () => { const gadm41 = apiAuthRequest .get(`${REQUEST_URL}?source[provider]=gadm&source[version]=4.1`) - .then(() => { - // Commenting this logic to avoid gadm 4.1 in production - // .then((areasResponse) => { - // const { data: areas } = areasResponse.data; - - // return areas.map((area) => parseArea(area)) + .then((areasResponse) => { + const { data: areas } = areasResponse.data; - return []; + return areas.map((area) => parseArea(area)); }); return Promise.all([gadm36, gadm41]).then(([areas36, areas41]) => { diff --git a/services/country.js b/services/country.js index 6892454f5f..41ad8675c5 100644 --- a/services/country.js +++ b/services/country.js @@ -1,27 +1,30 @@ import { all, spread } from 'axios'; -import { cartoRequest, dataRequest } from 'utils/request'; -import { getGadm36Id } from 'utils/gadm'; +import { dataRequest } from 'utils/request'; +import { getGadmId } from 'utils/gadm'; import countryLinks from './country-links.json'; -const SQL_QUERIES = { - getCountries: - "SELECT iso, name_engli as name FROM gadm36_countries WHERE iso != 'TWN' AND iso != 'XCA' ORDER BY name", - getFAOCountries: - 'SELECT iso, country AS name FROM data WHERE 1 = 1 AND year = 2020', - getRegions: - "SELECT gid_1 as id, name_1 as name FROM gadm36_adm1 WHERE iso = '{iso}' ORDER BY name ", - getSubRegions: - "SELECT gid_2 as id, name_2 as name FROM gadm36_adm2 WHERE iso = '{iso}' AND gid_1 = '{adm1}' AND type_2 NOT IN ('Waterbody', 'Water body', 'Water Body') ORDER BY name", +export const GADM_DATASET = + '/dataset/gadm_administrative_boundaries/v4.1.75/query'; + +export const SQL_QUERIES = { + getGADMCountries: + "SELECT country AS name, gid_0 AS iso FROM gadm_administrative_boundaries WHERE adm_level = '0' AND gid_0 NOT IN ('Z01', 'Z02', 'Z03', 'Z04', 'Z05', 'Z06', 'Z07', 'Z08', 'Z09', 'TWN', 'XCA') ORDER BY country", + getGADMRegions: + "SELECT name_1 AS name, gid_1 AS id FROM gadm_administrative_boundaries WHERE adm_level='1' AND gid_0 = '{iso}' ORDER BY name", + getGADMSubRegions: + "SELECT gid_2 as id, name_2 as name FROM gadm_administrative_boundaries WHERE gid_0 = '{iso}' AND gid_1 = '{adm1}' AND adm_level='2' AND type_2 NOT IN ('Waterbody', 'Water body', 'Water Body') ORDER BY name", + getFAOCountries: 'SELECT iso, country AS name FROM data WHERE year = 2020', }; const convertToOptions = (countries) => countries.map((c) => ({ label: c.name, value: c.iso })); export const getCountriesProvider = () => { - const url = `/sql?q=${SQL_QUERIES.getCountries}`; - return cartoRequest.get(url); + const url = `${GADM_DATASET}?sql=${SQL_QUERIES.getGADMCountries}`; + + return dataRequest.get(url); }; export const getFAOCountriesProvider = () => { @@ -30,15 +33,20 @@ export const getFAOCountriesProvider = () => { }; export const getRegionsProvider = ({ adm0, token }) => { - const url = `/sql?q=${SQL_QUERIES.getRegions}`.replace('{iso}', adm0); - return cartoRequest.get(url, { cancelToken: token }); + const url = `${GADM_DATASET}?sql=${SQL_QUERIES.getGADMRegions}`.replace( + '{iso}', + adm0 + ); + + return dataRequest.get(url, { cancelToken: token }); }; -export const getSubRegionsProvider = (adm0, adm1, token) => { - const url = `/sql?q=${SQL_QUERIES.getSubRegions}` +export const getSubRegionsProvider = ({ adm0, adm1, token }) => { + const url = `${GADM_DATASET}?sql=${SQL_QUERIES.getGADMSubRegions}` .replace('{iso}', adm0) - .replace('{adm1}', getGadm36Id(adm0, adm1)); - return cartoRequest.get(url, { cancelToken: token }); + .replace('{adm1}', getGadmId(adm0, adm1)); + + return dataRequest.get(url, { cancelToken: token }); }; export const getCountryLinksProvider = () => { @@ -63,17 +71,32 @@ export const getCountryLinksSerialized = async () => { export const getCategorisedCountries = (asOptions = false) => all([getCountriesProvider(), getFAOCountriesProvider()]).then( - spread((gadm36Countries, faoCountries) => { + spread((gadm41Countries, faoCountries) => { + // GADM 4.1 cut short names larger than 32 characters, + // We are enforcing the long name for these cases + const shortenedCountries = { + 'United States Minor Outlying Isl': + 'United States Minor Outlying Islands', + 'South Georgia and the South Sand': + 'South Georgia and the South Sandwich Islands', + }; + + const gadm41CountriesWithLongNames = gadm41Countries.data.map((country) => + shortenedCountries[country.name] + ? { ...country, name: shortenedCountries[country.name] } + : country + ); + return { gadmCountries: asOptions - ? convertToOptions(gadm36Countries.data.rows) - : gadm36Countries.data.rows, + ? convertToOptions(gadm41CountriesWithLongNames) + : gadm41CountriesWithLongNames, faoCountries: asOptions ? convertToOptions(faoCountries.data) : faoCountries.data, countries: asOptions - ? convertToOptions(gadm36Countries.data.rows) - : gadm36Countries.data.rows, + ? convertToOptions(gadm41CountriesWithLongNames) + : gadm41CountriesWithLongNames, }; }) ); diff --git a/services/geocoding.js b/services/geocoding.js index 54c1cc81d2..72319fc40a 100644 --- a/services/geocoding.js +++ b/services/geocoding.js @@ -1,81 +1,19 @@ -import { mapboxRequest, cartoRequest } from 'utils/request'; -import compact from 'lodash/compact'; -import { all, spread } from 'axios'; -import bbox from 'turf-bbox'; - -import { POLITICAL_BOUNDARIES } from 'data/layers'; - -const getSearchSQL = (string, nameString, nameStringSimple) => { - const words = string && string.split(/,| |, /); - if (words && words.length) { - const mappedWords = compact(words.map((w) => (w ? `%25${w}%25` : ''))); - const whereQueries = mappedWords.map( - (w) => - `LOWER(${nameString}) LIKE '${w}' OR LOWER(${nameStringSimple}) LIKE '${w}' OR LOWER(name_1) LIKE '${w}' OR LOWER(simple_name_1) LIKE '${w}' OR LOWER(name_2) LIKE '${w}' OR LOWER(simple_name_2) LIKE '${w}'` - ); - - return whereQueries.join(' OR '); - } - - return null; -}; - -const getWhereStatement = (search, nameString, nameStringSimple) => { - const searchLower = search && search.toLowerCase(); - - return getSearchSQL(searchLower, nameString, nameStringSimple); -}; - -const langCodes = { - en: '0', - fr: 'fr', - zh: 'zh', - id: '0', - es_MX: 'es_mx', - pt_BR: '0', -}; +import { mapboxRequest } from 'utils/request'; export const fetchGeocodeLocations = ( searchQuery = '', lang = 'en', cancelToken ) => { - const nameString = `name_${langCodes[lang]}`; - let nameStringSimple = 'simple_name_0'; - if (lang !== 'en') nameStringSimple = nameString; - const whereStatement = getWhereStatement( - searchQuery, - nameString, - nameStringSimple - ); - - return all([ - mapboxRequest - .get( - `/geocoding/v5/mapbox.places/${searchQuery}.json?language=${lang}&access_token=${process.env.MapboxAccessToken}&types=postcode,district,place,locality,neighborhood,address,poi`, - { - cancelToken, - } - ) - .catch(() => {}), - cartoRequest - .get( - `/sql?q=SELECT bbox, centroid, cartodb_id, area, size, level, name_0, name_1, name_2, gid_0, gid_1, gid_2, CASE WHEN gid_2 is not null THEN CONCAT(name_2, ', ', name_1, ', ', ${nameString}) WHEN gid_1 is not null THEN CONCAT(name_1, ', ', ${nameString}) WHEN gid_0 is not null THEN ${nameString} END AS place_name FROM gadm36_political_boundaries WHERE ${whereStatement} AND gid_0 != 'TWN' AND gid_0 != 'XCA' ORDER BY level, place_name LIMIT 5`, - { - cancelToken, - } - ) - .catch(() => {}), - ]).then( - spread((mapboxResponse, cartoResponse) => { - const boundaries = cartoResponse?.data?.rows?.map((c) => ({ - ...c, - id: POLITICAL_BOUNDARIES, - bbox: bbox(JSON.parse(c.bbox)), - center: JSON.parse(c.centroid)?.coordinates, - })); - - return boundaries.concat(mapboxResponse?.data?.features); + return mapboxRequest + .get( + `/geocoding/v5/mapbox.places/${searchQuery}.json?language=${lang}&access_token=${process.env.NEXT_PUBLIC_MAPBOX_TOKEN}`, + { + cancelToken, + } + ) + .then((mapboxResponse) => { + return mapboxResponse?.data?.features; }) - ); + .catch(() => {}); }; diff --git a/services/geostore.js b/services/geostore.js index e52a850466..9041b6e8a7 100644 --- a/services/geostore.js +++ b/services/geostore.js @@ -52,7 +52,7 @@ const setThreshold = (iso, adm1, adm2) => { */ const fetchGeostore = ({ url, token, queryParams = '' }) => { return dataRequest - .get(`https://data-api.globalforestwatch.org${url}?${queryParams}`, { + .get(`${url}?${queryParams}`, { cancelToken: token, }) .then((response) => { @@ -70,15 +70,16 @@ export const getGeostore = ({ type, adm0, adm1, adm2, token }) => { if (!type || !adm0) return null; const sourceProvider = 'source[provider]=gadm'; - const sourceVersion = 'source[version]=3.6'; + const sourceVersion = 'source[version]=4.1'; const threshold = `simplify=${setThreshold(adm0, adm1, adm2)}`; const queryParams = `${sourceProvider}&${sourceVersion}`; switch (type) { case 'country': return fetchGeostore({ - url: `/geostore/admin/${adm0}${adm1 ? `/${adm1}` : ''}${adm2 ? `/${adm2}` : '' - }`, + url: `/geostore/admin/${adm0}${adm1 ? `/${adm1}` : ''}${ + adm2 ? `/${adm2}` : '' + }`, queryParams: `${queryParams}&${threshold}`, token, }); diff --git a/services/location.js b/services/location.js index f0d8834bd8..55df71ae28 100644 --- a/services/location.js +++ b/services/location.js @@ -1,44 +1,71 @@ import lowerCase from 'lodash/lowerCase'; import startCase from 'lodash/startCase'; -import { cartoRequest } from 'utils/request'; + import { getGeodescriberByGeostore } from 'services/geodescriber'; import { getDatasetQuery } from 'services/datasets'; import { getArea } from 'services/areas'; +import { + getCountriesProvider, + getRegionsProvider, + getSubRegionsProvider, +} from 'services/country'; + +const findByIso = (list, iso) => list?.find((item) => item?.iso === iso); +const findById = (list, idPrefix) => + list?.find((item) => item?.id.startsWith(idPrefix)); + +const getBaseLocationData = async ({ adm0, adm1, adm2 }) => { + const [countriesData, regionsData, subRegionsData] = await Promise.all([ + getCountriesProvider(), + adm1 ? getRegionsProvider({ adm0 }) : null, + adm2 ? getSubRegionsProvider({ adm0, adm1 }) : null, + ]); + + const { data: countries } = countriesData || {}; + const { data: regions } = regionsData || {}; + const { data: subRegions } = subRegionsData || {}; + + const country = findByIso(countries, adm0); + const region = adm1 ? findById(regions, `${adm0}.${adm1}_`) : null; + const subRegion = adm2 + ? findById(subRegions, `${adm0}.${adm1}.${adm2}_`) + : null; + + return { country, region, subRegion }; +}; export const countryConfig = { - adm0: (params) => - cartoRequest( - `/sql?q=SELECT iso, name_engli as name FROM gadm36_countries WHERE iso = '${params.adm0}' AND iso != 'XCA' AND iso != 'TWN'` - ).then((response) => { - const { name, ...props } = response?.data?.rows?.[0]; - - return { - locationName: name, - ...props, - }; - }), - adm1: (params) => - cartoRequest( - `/sql?q=SELECT iso, gid_1 as id, name_0 as adm0, name_1 as adm1 FROM gadm36_adm1 WHERE gid_1 = '${params.adm0}.${params.adm1}_1' AND iso != 'XCA' AND iso != 'TWN'` - ).then((response) => { - const { adm1, adm0, ...props } = response?.data?.rows?.[0]; - - return { - locationName: `${adm1}, ${adm0}`, - ...props, - }; - }), - adm2: (params) => - cartoRequest( - `/sql?q=SELECT gid_2, name_0 as adm0, name_1 as adm1, name_2 as adm2 FROM gadm36_adm2 WHERE gid_2 = '${params.adm0}.${params.adm1}.${params.adm2}_1' AND iso != 'XCA' AND iso != 'TWN'` - ).then((response) => { - const { adm2, adm1, adm0, ...props } = response?.data?.rows?.[0]; - - return { - locationName: `${adm2}, ${adm1}, ${adm0}`, - ...props, - }; - }), + adm0: async ({ adm0 }) => { + const { country } = await getBaseLocationData({ adm0 }); + const { name, ...props } = country || {}; + + return { + locationName: name, + ...props, + }; + }, + adm1: async ({ adm0, adm1 }) => { + const { country, region } = await getBaseLocationData({ adm0, adm1 }); + const { name, ...props } = region || {}; + + return { + locationName: `${name}, ${country?.name}`, + ...props, + }; + }, + adm2: async ({ adm0, adm1, adm2 }) => { + const { country, region, subRegion } = await getBaseLocationData({ + adm0, + adm1, + adm2, + }); + const { name, ...props } = subRegion || {}; + + return { + locationName: `${name}, ${country?.name}, ${region?.name}`, + ...props, + }; + }, }; export const geostoreConfig = { diff --git a/services/projects.js b/services/projects.js index 664adfbddc..012bb57a70 100644 --- a/services/projects.js +++ b/services/projects.js @@ -37,7 +37,7 @@ apiFetch.setFetchHandler(async (options) => { }); const formatProjects = (projectsData) => { - const countries = projectsData?.[1]?.data?.rows; + const countries = projectsData?.[1]?.data; const projects = projectsData?.[0]?.data; if (!projects) { diff --git a/utils/__tests__/gadm.js b/utils/__tests__/gadm.js new file mode 100644 index 0000000000..d7cfb48eef --- /dev/null +++ b/utils/__tests__/gadm.js @@ -0,0 +1,71 @@ +import { beforeAll } from '@jest/globals'; +import { getGadmId, parseGadmId, getGadmLocationByLevel } from 'utils/gadm'; + +describe('getGadmId', () => { + it('should generate the correct GADM ID for country and region levels', () => { + expect(getGadmId('BRA', '25')).toBe('BRA.25_1'); + }); + + it('should generate the correct GADM ID for country, region, and sub-region levels', () => { + expect(getGadmId('BRA', '25', '390')).toBe('BRA.25.390_1'); + }); +}); + +describe('parseGadmId', () => { + it('should parse a full GADM ID correctly', () => { + const gid = 'BRA.25.390'; + expect(parseGadmId(gid)).toEqual({ + adm0: 'BRA', + adm1: 25, + adm2: 390, + }); + }); + + it('should parse a GADM ID with only country and region correctly', () => { + const gid = 'BRA.25_1'; + expect(parseGadmId(gid)).toEqual({ + adm0: 'BRA', + adm1: 25, + adm2: undefined, + }); + }); +}); + +describe('getGadmLocationByLevel', () => { + let location; + + beforeAll(() => { + location = { + gid_0: 'BRA', + gid_1: 'BRA.25_1', + gid_2: 'BRA.25.390_1', + }; + }); + + it('should return location with parsed GADM ID for adm_level 0', () => { + expect(getGadmLocationByLevel({ adm_level: 0, ...location })).toEqual({ + type: 'country', + adm0: 'BRA', + adm1: undefined, + adm2: undefined, + }); + }); + + it('should return location with parsed GADM ID for level 1', () => { + expect(getGadmLocationByLevel({ adm_level: 1, ...location })).toEqual({ + type: 'country', + adm0: 'BRA', + adm1: 25, + adm2: undefined, + }); + }); + + it('should return location with parsed GADM ID for level 2', () => { + expect(getGadmLocationByLevel({ adm_level: 2, ...location })).toEqual({ + type: 'country', + adm0: 'BRA', + adm1: 25, + adm2: 390, + }); + }); +}); diff --git a/utils/gadm.js b/utils/gadm.js index 754a0fd252..708bc7a427 100644 --- a/utils/gadm.js +++ b/utils/gadm.js @@ -1,9 +1,9 @@ -export const getGadm36Id = (country, region, subRegion) => +export const getGadmId = (country, region, subRegion) => `${country}${region ? `.${region}` : ''}${ subRegion ? `.${subRegion}_1` : '_1' }`; -export const parseGadm36Id = (gid) => { +export const parseGadmId = (gid) => { if (!gid) return null; const ids = gid.split('.'); @@ -26,6 +26,6 @@ export const parseGadm36Id = (gid) => { export const getGadmLocationByLevel = ({ adm_level, ...location }) => ({ type: 'country', ...(location?.gid_0 && { - ...parseGadm36Id(location[`gid_${adm_level || '0'}`]), + ...parseGadmId(location[`gid_${adm_level || '0'}`]), }), }); diff --git a/utils/request.js b/utils/request.js index 73b1f2c8af..89b2ba3e9d 100644 --- a/utils/request.js +++ b/utils/request.js @@ -53,10 +53,16 @@ export const dataRequest = axios.create({ baseURL: DATA_API_URL, headers: { 'x-api-key': DATA_API_KEY, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache', + Expires: '0', }, }), ...(!isServer && { baseURL: PROXIES.DATA_API, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + Pragma: 'no-cache', + Expires: '0', }), transformResponse: [(data) => JSON.parse(data)?.data], });