diff --git a/apps/fishing-map/features/reports/activity/ReportActivityGraph.tsx b/apps/fishing-map/features/reports/activity/ReportActivityGraph.tsx index be9114d64b..45c5f260c3 100644 --- a/apps/fishing-map/features/reports/activity/ReportActivityGraph.tsx +++ b/apps/fishing-map/features/reports/activity/ReportActivityGraph.tsx @@ -10,18 +10,14 @@ import { useReportFeaturesLoading, useReportFilteredTimeSeries, } from 'features/reports/activity/reports-activity-timeseries.hooks' -import { - selectReportArea, - selectTimeComparisonValues, -} from 'features/reports/areas/reports.selectors' +import { selectTimeComparisonValues } from 'features/reports/areas/reports.selectors' import ReportActivityPlaceholder from 'features/reports/areas/placeholders/ReportActivityPlaceholder' import ReportActivityPeriodComparison from 'features/reports/activity/ReportActivityPeriodComparison' import ReportActivityPeriodComparisonGraph from 'features/reports/activity/ReportActivityPeriodComparisonGraph' import UserGuideLink from 'features/help/UserGuideLink' -import { AsyncReducerStatus } from 'utils/async-slice' +import { useFitAreaInViewport, useReportAreaBounds } from 'features/reports/areas/reports.hooks' import { selectReportActivityGraph } from '../areas/reports.config.selectors' import { ReportActivityGraph } from '../areas/reports.types' -import { useFetchReportArea, useFitAreaInViewport } from '../areas/reports.hooks' import ReportActivityEvolution from './ReportActivityEvolution' import ReportActivityBeforeAfter from './ReportActivityBeforeAfter' import ReportActivityBeforeAfterGraph from './ReportActivityBeforeAfterGraph' @@ -48,21 +44,17 @@ const emptyGraphData = {} as ReportGraphProps export default function ReportActivity() { useComputeReportTimeSeries() - const reportArea = useSelector(selectReportArea) - const { status } = useFetchReportArea() const fitAreaInViewport = useFitAreaInViewport() + const { loaded, bbox } = useReportAreaBounds() // This ensures that the area is in viewport when then area load finishes useEffect(() => { - if (status === AsyncReducerStatus.Finished && reportArea?.bounds) { - requestAnimationFrame(() => { - fitAreaInViewport() - }) + if (loaded && bbox?.length) { + fitAreaInViewport() } - // Reacting only to the area status and fitting bounds after load // eslint-disable-next-line react-hooks/exhaustive-deps - }, [status, reportArea]) + }, [loaded]) const { t } = useTranslation() const { start, end } = useTimerangeConnect() diff --git a/apps/fishing-map/features/reports/areas/reports.hooks.ts b/apps/fishing-map/features/reports/areas/reports.hooks.ts index 1ebc934b5c..ea1b9a477b 100644 --- a/apps/fishing-map/features/reports/areas/reports.hooks.ts +++ b/apps/fishing-map/features/reports/areas/reports.hooks.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo } from 'react' import { useSelector } from 'react-redux' +import { useGetStatsByDataviewQuery } from 'queries/stats-api' import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { Dataset, Dataview } from '@globalfishingwatch/api-types' import { useLocalStorage } from '@globalfishingwatch/react-hooks' @@ -21,6 +22,7 @@ import { selectReportArea, selectReportAreaDataviews, selectReportAreaIds, + selectReportAreaStatus, selectReportDataviewsWithPermissions, } from 'features/reports/areas/reports.selectors' import { useDeckMap } from 'features/map/map-context.hooks' @@ -30,7 +32,7 @@ import { FIT_BOUNDS_REPORT_PADDING } from 'data/config' import { RFMO_DATAVIEW_SLUG } from 'data/workspaces' import { getMapCoordinatesFromBounds } from 'features/map/map-bounds.hooks' import { LAST_REPORTS_STORAGE_KEY, LastReportStorage } from 'features/reports/areas/reports.config' -import { selectIsVesselGroupReportLocation } from 'routes/routes.selectors' +import { selectIsVesselGroupReportLocation, selectUrlTimeRange } from 'routes/routes.selectors' import { AsyncReducerStatus } from 'utils/async-slice' import { fetchReportVesselsThunk, @@ -79,10 +81,47 @@ function useReportAreaCenter(bounds?: Bbox) { }, [bounds, map]) } +function useVesselGroupReportBounds() { + const isVesselGroupReportLocation = useSelector(selectIsVesselGroupReportLocation) + const dataview = useSelector(selectVGRActivityDataview)! + const urlTimeRange = useSelector(selectUrlTimeRange) + const { + data: stats, + isFetching, + isSuccess, + } = useGetStatsByDataviewQuery( + { + dataview, + timerange: urlTimeRange as any, + fields: [], + }, + { + skip: !isVesselGroupReportLocation || !dataview || !urlTimeRange, + } + ) + + const statsBbox = stats && ([stats.minLon, stats.minLat, stats.maxLon, stats.maxLat] as Bbox) + return { + loaded: !isFetching && isSuccess, + bbox: statsBbox?.some((v) => v === null || v === undefined) ? null : statsBbox!, + } +} + +export function useReportAreaBounds() { + const isVesselGroupReportLocation = useSelector(selectIsVesselGroupReportLocation) + const { loaded, bbox } = useVesselGroupReportBounds() + const area = useSelector(selectReportArea) + const areaStatus = useSelector(selectReportAreaStatus) + const areaBbox = isVesselGroupReportLocation ? bbox : area?.geometry?.bbox || area!?.bounds + return { + loaded: isVesselGroupReportLocation ? loaded : areaStatus === AsyncReducerStatus.Finished, + bbox: areaBbox, + } +} + export function useReportAreaInViewport() { const viewState = useMapViewState() - const area = useSelector(selectReportArea) - const bbox = area?.geometry?.bbox || area!?.bounds + const { bbox } = useReportAreaBounds() const areaCenter = useReportAreaCenter(bbox as Bbox) return ( viewState?.latitude === areaCenter?.latitude && @@ -93,8 +132,7 @@ export function useReportAreaInViewport() { export function useFitAreaInViewport() { const setMapCoordinates = useSetMapCoordinates() - const area = useSelector(selectReportArea) - const bbox = area?.geometry?.bbox || area!?.bounds + const { bbox } = useReportAreaBounds() const areaCenter = useReportAreaCenter(bbox as Bbox) const areaInViewport = useReportAreaInViewport() return useCallback(() => { diff --git a/apps/fishing-map/features/reports/vessel-groups/activity/VGRActivitySubsectionSelector.tsx.tsx b/apps/fishing-map/features/reports/vessel-groups/activity/VGRActivitySubsectionSelector.tsx.tsx index 0d299588e0..33330f6c54 100644 --- a/apps/fishing-map/features/reports/vessel-groups/activity/VGRActivitySubsectionSelector.tsx.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/activity/VGRActivitySubsectionSelector.tsx.tsx @@ -5,12 +5,17 @@ import { useLocationConnect } from 'routes/routes.hook' import { VGRActivitySubsection } from 'features/vessel-groups/vessel-groups.types' import { selectVGRActivitySubsection } from 'features/reports/vessel-groups/vessel-group.config.selectors' import { useReportFeaturesLoading } from 'features/reports/activity/reports-activity-timeseries.hooks' +import { useFitAreaInViewport } from 'features/reports/areas/reports.hooks' +import { resetReportData } from 'features/reports/activity/reports-activity.slice' +import { useAppDispatch } from 'features/app/app.hooks' function VGRActivitySubsectionSelector() { const { t } = useTranslation() + const dispatch = useAppDispatch() const { dispatchQueryParams } = useLocationConnect() const subsection = useSelector(selectVGRActivitySubsection) const loading = useReportFeaturesLoading() + const fitAreaInViewport = useFitAreaInViewport() const options: ChoiceOption[] = [ { id: 'fishing-effort', @@ -31,6 +36,8 @@ function VGRActivitySubsectionSelector() { // action: `Click on ${option.id} activity graph`, // }) dispatchQueryParams({ vGRActivitySubsection: option.id }) + fitAreaInViewport() + dispatch(resetReportData()) } } diff --git a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts index 2e502c6641..4f321bc7d2 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessel-group-report.selectors.ts @@ -5,7 +5,7 @@ import { VesselGroupInsightParams, } from 'queries/vessel-insight-api' import { RootState } from 'reducers' -import { InsightType } from '@globalfishingwatch/api-types' +import { DataviewCategory, InsightType } from '@globalfishingwatch/api-types' import { selectDataviewInstancesResolvedVisible } from 'features/dataviews/selectors/dataviews.instances.selectors' import { selectReportVesselGroupId } from 'routes/routes.selectors' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' @@ -28,13 +28,21 @@ export const IUU_INSIGHT_ID = 'VESSEL-IDENTITY-IUU-VESSEL-LIST' as InsightType export const FLAG_CHANGE_INSIGHT_ID = 'VESSEL-IDENTITY-FLAG-CHANGES' as InsightType export const MOU_INSIGHT_ID = 'VESSEL-IDENTITY-MOU-LIST' as InsightType -export const selectVGRDataview = createSelector( +export const selectAllVGRDataviews = createSelector( [selectDataviewInstancesResolvedVisible, selectReportVesselGroupId], (dataviews, reportVesselGroupId) => { - return dataviews?.find((dataview) => dataviewHasVesselGroupId(dataview, reportVesselGroupId)) + return dataviews?.filter((dataview) => dataviewHasVesselGroupId(dataview, reportVesselGroupId)) } ) +export const selectVGRDataview = createSelector([selectAllVGRDataviews], (dataviews) => { + return dataviews?.find((dataview) => dataview.category === DataviewCategory.VesselGroups) +}) + +export const selectVGRActivityDataview = createSelector([selectAllVGRDataviews], (dataviews) => { + return dataviews?.find((dataview) => dataview.category === DataviewCategory.Activity) +}) + export const selectVGREventsSubsectionDataview = createSelector( [ selectEventsDataviews, diff --git a/apps/fishing-map/queries/stats-api.ts b/apps/fishing-map/queries/stats-api.ts index c41bee7c24..15bbf84ea9 100644 --- a/apps/fishing-map/queries/stats-api.ts +++ b/apps/fishing-map/queries/stats-api.ts @@ -5,13 +5,13 @@ import { uniq } from 'es-toolkit' import { DateTime } from 'luxon' import type { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { getDatasetsExtent } from '@globalfishingwatch/datasets-client' -import { StatField, StatFields, StatType, StatsParams } from '@globalfishingwatch/api-types' +import { StatFields, StatType, StatsParams } from '@globalfishingwatch/api-types' import type { TimeRange } from 'features/timebar/timebar.slice' type FetchDataviewStatsParams = { timerange: TimeRange dataview: UrlDataviewInstance - fields?: StatField[] + fields?: StatsParams[] } interface CustomBaseQueryArg extends FetchBaseQueryArgs { @@ -26,7 +26,6 @@ const serializeStatsDataviewKey: SerializeQueryArgs = ({ que ].join('-') } -const DEFAULT_STATS_FIELDS: StatsParams[] = ['VESSEL-IDS', 'FLAGS'] // Define a service using a base URL and expected endpoints export const dataviewStatsApi = createApi({ reducerPath: 'dataviewStatsApi', @@ -36,7 +35,7 @@ export const dataviewStatsApi = createApi({ endpoints: (builder) => ({ getStatsByDataview: builder.query({ serializeQueryArgs: serializeStatsDataviewKey, - query: ({ dataview, timerange, fields = DEFAULT_STATS_FIELDS }) => { + query: ({ dataview, timerange, fields }) => { const datasets = dataview.datasets?.filter((dataset) => dataview.config?.datasets?.includes(dataset.id) ) @@ -52,17 +51,25 @@ export const dataviewStatsApi = createApi({ ) const statsParams = { datasets: [dataview.config?.datasets?.join(',') || []], - filters: [dataview.config?.filter] || [], - 'vessel-groups': [dataview.config?.['vessel-groups']] || [], 'date-range': [laterStartDate.toISODate(), earlierEndDate.toISODate()].join(','), + ...(fields?.length && { + fields: fields.join(','), + }), + ...(dataview.config?.filter && { filters: [dataview.config?.filter] || [] }), + ...(dataview.config?.['vessel-groups'] && { + 'vessel-groups': [dataview.config?.['vessel-groups']] || [], + }), } return { - url: `?fields=${fields.join(',')}&${stringify(statsParams, { + url: `?${stringify(statsParams, { arrayFormat: 'indices', })}`, } }, transformResponse: (response: StatFields[], meta, args) => { + if (!args.fields?.length) { + return response?.[0] + } const units = uniq(args?.dataview?.datasets?.flatMap((d) => d.unit || []) || []) if (units.length > 1) { console.warn('Incompatible datasets stats unit, using the first type', units[0])