diff --git a/apps/fishing-map/features/datasets/datasets.selectors.ts b/apps/fishing-map/features/datasets/datasets.selectors.ts index 44fe00a233..e418daf3f1 100644 --- a/apps/fishing-map/features/datasets/datasets.selectors.ts +++ b/apps/fishing-map/features/datasets/datasets.selectors.ts @@ -1,6 +1,7 @@ import { createSelector } from '@reduxjs/toolkit' import { uniqBy } from 'es-toolkit' -import { DatasetCategory, DatasetTypes } from '@globalfishingwatch/api-types' +import { DatasetCategory, DatasetStatus, DatasetTypes } from '@globalfishingwatch/api-types' +import { VESSEL_GROUPS_MIN_API_VERSION } from 'features/vessel-groups/vessel-groups.config' import { selectAllDatasets } from './datasets.slice' const EMPTY_ARRAY: [] = [] @@ -25,6 +26,17 @@ const selectDatasetsByType = (type: DatasetTypes) => { export const selectFourwingsDatasets = selectDatasetsByType(DatasetTypes.Fourwings) export const selectVesselsDatasets = selectDatasetsByType(DatasetTypes.Vessels) +export const selectVesselGroupCompatibleDatasets = createSelector( + [selectVesselsDatasets], + (datasets) => { + return datasets.filter( + (d) => + d.status !== DatasetStatus.Deleted && + d.configuration?.apiSupportedVersions?.includes(VESSEL_GROUPS_MIN_API_VERSION) + ) + } +) + export const selectActivityDatasets = createSelector([selectFourwingsDatasets], (datasets) => { return datasets.filter((d) => d.category === DatasetCategory.Activity) }) diff --git a/apps/fishing-map/features/reports/events/VGREvents.tsx b/apps/fishing-map/features/reports/events/VGREvents.tsx index 4a6e6ed736..958f3b5ccf 100644 --- a/apps/fishing-map/features/reports/events/VGREvents.tsx +++ b/apps/fishing-map/features/reports/events/VGREvents.tsx @@ -29,6 +29,7 @@ import { selectVGREventsVesselsFlags, selectVGREventsVesselsGrouped, } from 'features/reports/events/vgr-events.selectors' +import { formatI18nNumber } from 'features/i18n/i18nNumber' import styles from './VGREvents.module.css' function VGREvents() { @@ -37,7 +38,7 @@ function VGREvents() { const filter = useSelector(selectVGREventsVesselFilter) const eventsDataview = useSelector(selectVGREventsSubsectionDataview) const vesselsGroupByProperty = useSelector(selectVGREventsVesselsProperty) - const vessels = useSelector(selectVGREventsVessels) + const vesselsWithEvents = useSelector(selectVGREventsVessels) const vesselFlags = useSelector(selectVGREventsVesselsFlags) const vesselGroups = useSelector(selectVGREventsVesselsGrouped) @@ -87,8 +88,8 @@ function VGREvents() { t('vesselGroup.summaryEvents', { defaultValue: '{{vessels}} vessels from {{flags}} flags had {{activityQuantity}} {{activityUnit}} globally between {{start}} and {{end}}', - vessels: vessels?.length, - flags: vesselFlags, + vessels: formatI18nNumber(vesselsWithEvents?.length || 0), + flags: vesselFlags?.size, activityQuantity: data.timeseries.reduce((acc, group) => acc + group.value, 0), activityUnit: `${eventsDataview?.datasets?.[0]?.subcategory?.toLowerCase()} ${t( 'common.events', diff --git a/apps/fishing-map/features/reports/events/vgr-events.selectors.ts b/apps/fishing-map/features/reports/events/vgr-events.selectors.ts index 3b087978fe..092dba4be8 100644 --- a/apps/fishing-map/features/reports/events/vgr-events.selectors.ts +++ b/apps/fishing-map/features/reports/events/vgr-events.selectors.ts @@ -19,8 +19,10 @@ import { getVesselsFiltered } from 'features/reports/areas/area-reports.utils' import { REPORT_FILTER_PROPERTIES } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { selectVGREventsSubsectionDataview } from 'features/reports/vessel-groups/vessel-group-report.selectors' import { OTHER_CATEGORY_LABEL } from 'features/reports/vessel-groups/vessel-group-report.config' -import { formatInfoField } from 'utils/info' +import { EMPTY_FIELD_PLACEHOLDER, formatInfoField } from 'utils/info' import { MAX_CATEGORIES } from 'features/reports/areas/area-reports.config' +import { t } from 'features/i18n/i18n' +import { getVesselsWithoutDuplicates } from 'features/vessel-groups/vessel-groups.utils' export const selectFetchVGREventsVesselsParams = createSelector( [selectTimeRange, selectReportVesselGroupId, selectVGREventsSubsectionDataview], @@ -53,7 +55,8 @@ export const selectVGREventsVessels = createSelector( if (!data || !vesselGroup) { return } - const insightVessels = vesselGroup?.vessels?.flatMap((vessel) => { + const vesselsWithoutDuplicates = getVesselsWithoutDuplicates(vesselGroup.vessels) + const insightVessels = vesselsWithoutDuplicates?.flatMap((vessel) => { const vesselWithEvents = data?.find((v) => v.vesselId === vessel.vesselId) if (!vesselWithEvents) { return [] @@ -68,7 +71,7 @@ export const selectVGREventsVessels = createSelector( .sort() .map((g) => formatInfoField(g, 'geartypes')) .join(', ') || OTHER_CATEGORY_LABEL, - flagTranslated: formatInfoField(identity.flag, 'flag'), + flagTranslated: t(`flags:${identity.flag as string}` as any), } }) return insightVessels.sort((a, b) => b.numEvents - a.numEvents) @@ -90,39 +93,69 @@ export const selectVGREventsVesselsPaginated = createSelector( return vessels.slice(resultsPerPage * page, resultsPerPage * (page + 1)) } ) +type GraphDataGroup = { + name: string + value: number +} export const selectVGREventsVesselsGrouped = createSelector( [selectVGREventsVesselsFiltered, selectVGREventsVesselsProperty], (vessels, property) => { if (!vessels?.length) return [] - const groups: { name: string; value: number }[] = Object.entries( + const orderedGroups: { name: string; value: number }[] = Object.entries( groupBy(vessels, (vessel) => { - return property === 'flag' ? (vessel.flagTranslated as string) : (vessel.geartype as string) + return property === 'flag' ? vessel.flagTranslated : (vessel.geartype as string) }) ) .map(([key, value]) => ({ name: key, property: key, value: value.length })) .sort((a, b) => b.value - a.value) - if (groups.length <= MAX_CATEGORIES) { - return groups + const groupsWithoutOther: GraphDataGroup[] = [] + const otherGroups: GraphDataGroup[] = [] + orderedGroups.forEach((group) => { + if ( + group.name === 'null' || + group.name.toLowerCase() === OTHER_CATEGORY_LABEL.toLowerCase() || + group.name === EMPTY_FIELD_PLACEHOLDER + ) { + otherGroups.push(group) + } else { + groupsWithoutOther.push(group) + } + }) + const allGroups = + otherGroups.length > 0 + ? [ + ...groupsWithoutOther, + { + name: OTHER_CATEGORY_LABEL, + value: otherGroups.reduce((acc, group) => acc + group.value, 0), + }, + ] + : groupsWithoutOther + if (allGroups.length <= MAX_CATEGORIES) { + return allGroups } - - const firstNine = groups.slice(0, MAX_CATEGORIES) - const other = groups.slice(MAX_CATEGORIES) - + const firstGroups = allGroups.slice(0, MAX_CATEGORIES) + const restOfGroups = allGroups.slice(MAX_CATEGORIES) return [ - ...firstNine, + ...firstGroups, { name: OTHER_CATEGORY_LABEL, - property: other.map((g) => g.name).join(', '), - value: other.reduce((acc, group) => acc + group.value, 0), + value: restOfGroups.reduce((acc, group) => acc + group.value, 0), }, - ] + ] as GraphDataGroup[] } ) export const selectVGREventsVesselsFlags = createSelector([selectVGREventsVessels], (vessels) => { - if (!vessels?.length) return [] - return Object.keys(groupBy(vessels, (v) => v.flag)).length + if (!vessels?.length) return null + let flags = new Set() + vessels.forEach((vessel) => { + if (vessel.flagTranslated && vessel.flagTranslated !== 'null') { + flags.add(vessel.flagTranslated as string) + } + }) + return flags }) export const selectVGREventsVesselsPagination = createSelector( diff --git a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx index 805eeb26a0..ac2248447d 100644 --- a/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/VesselGroupReportTitle.tsx @@ -14,7 +14,6 @@ import { setVesselGroupModalVessels, setVesselGroupsModalOpen, } from 'features/vessel-groups/vessel-groups-modal.slice' -import { formatInfoField } from 'utils/info' import { useLocationConnect } from 'routes/routes.hook' import { selectHasOtherVesselGroupDataviews } from 'features/dataviews/selectors/dataviews.selectors' import { @@ -23,6 +22,8 @@ import { selectVGRVesselsTimeRange, } from 'features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors' import { formatI18nDate } from 'features/i18n/i18nDate' +import { formatI18nNumber } from 'features/i18n/i18nNumber' +import { getVesselGroupVesselsCount } from 'features/vessel-groups/vessel-groups.utils' import styles from './VesselGroupReportTitle.module.css' import { VesselGroupReport } from './vessel-group-report.slice' import { selectViewOnlyVesselGroup } from './vessel-group.config.selectors' @@ -94,7 +95,7 @@ export default function VesselGroupReportTitle({ vesselGroup, loading }: ReportT t('vesselGroup.summary', { defaultValue: '{{vessels}} vessels from {{flags}} flags active from {{start}} to {{end}}', - vessels: vessels?.length, + vessels: formatI18nNumber(getVesselGroupVesselsCount(vesselGroup)), flags: flags?.size, start: formatI18nDate(timeRange.start, { format: DateTime.DATE_MED, diff --git a/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts index 1f55db3344..6b6e88c234 100644 --- a/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/insights/vessel-group-report-insights.selectors.ts @@ -12,8 +12,9 @@ import { VesselGroupInsight, VesselGroupInsightResponse, } from '@globalfishingwatch/api-types' -import { getSearchIdentityResolved, getVesselId } from 'features/vessel/vessel.utils' +import { getSearchIdentityResolved } from 'features/vessel/vessel.utils' import { VesselLastIdentity } from 'features/search/search.slice' +import { getVesselsWithoutDuplicates } from 'features/vessel-groups/vessel-groups.utils' import { selectVGRFishingInsightData, selectVGRFlagChangeInsightData, @@ -36,7 +37,8 @@ export const selectVGRVesselsByInsight = ( if (!data || !vesselGroup) { return [] } - const insightVessels = vesselGroup?.vessels?.flatMap((vessel) => { + const vesselsWithoutDuplicates = getVesselsWithoutDuplicates(vesselGroup.vessels) + const insightVessels = vesselsWithoutDuplicates.flatMap((vessel) => { const vesselWithInsight = data?.[insightProperty]?.find((v) => v.vesselId === vessel.vesselId) if (!vesselWithInsight || (insightCounter && get(vesselWithInsight, insightCounter) === 0)) { return [] diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx index 379156ec3c..ee4c45f3eb 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/VesselGroupReportVesselsTableFooter.tsx @@ -9,10 +9,9 @@ import I18nNumber from 'features/i18n/i18nNumber' import { useLocationConnect } from 'routes/routes.hook' import { selectTimeRange } from 'features/app/selectors/app.timebar.selectors' import { REPORT_SHOW_MORE_VESSELS_PER_PAGE, REPORT_VESSELS_PER_PAGE } from 'data/config' -import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { selectVGRData } from 'features/reports/vessel-groups/vessel-group-report.slice' -import { formatInfoField } from 'utils/info' import { selectVGRVesselFilter } from 'features/reports/vessel-groups/vessel-group.config.selectors' +import { getVesselProperty } from 'features/vessel/vessel.utils' import styles from './VesselGroupReportVesselsTableFooter.module.css' import { selectVGRVesselsFiltered, @@ -32,20 +31,17 @@ export default function VesselGroupReportVesselsTableFooter() { const onDownloadVesselsClick = () => { const vessels = allVessels?.map((vessel) => { - const vesselRegistryInfo = !!vessel.identity?.registryInfo?.length - ? vessel.identity?.registryInfo[0] - : null return { dataset: vessel.dataset, - flag: vesselRegistryInfo?.flag, + flag: getVesselProperty(vessel.identity!, 'flag'), 'flag translated': vessel.flagTranslated, 'GFW vessel type': vessel.vesselType, 'GFW gear type': vessel.geartype, sources: vessel.source, name: vessel.shipName, - MMSI: vesselRegistryInfo?.ssvid, - IMO: vesselRegistryInfo?.imo, - 'call sign': vesselRegistryInfo?.callsign, + MMSI: getVesselProperty(vessel.identity!, 'ssvid'), + IMO: getVesselProperty(vessel.identity!, 'imo'), + 'call sign': getVesselProperty(vessel.identity!, 'callsign'), vesselId: vessel.vesselId, } }) diff --git a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts index 41e7414321..099253d2b3 100644 --- a/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts +++ b/apps/fishing-map/features/reports/vessel-groups/vessels/vessel-group-report-vessels.selectors.ts @@ -111,7 +111,7 @@ export const selectVGRVesselsFlags = createSelector([selectVGRVesselsParsed], (v if (!vessels?.length) return null let flags = new Set() vessels.forEach((vessel) => { - if (vessel.flagTranslated) { + if (vessel.flagTranslated && vessel.flagTranslated !== 'null') { flags.add(vessel.flagTranslated) } }) diff --git a/apps/fishing-map/features/search/SearchActions.tsx b/apps/fishing-map/features/search/SearchActions.tsx index d7eb40f9ef..bb1b50ee36 100644 --- a/apps/fishing-map/features/search/SearchActions.tsx +++ b/apps/fishing-map/features/search/SearchActions.tsx @@ -84,7 +84,11 @@ function SearchActions() { return ( - +