From 3e5bd13dd9c16d5ec1da244f318389f4984ea5e1 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 21 Oct 2024 16:28:12 +0200 Subject: [PATCH 1/6] fix cluster line width --- .../src/layers/fourwings/clusters/FourwingsClustersLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts b/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts index 3f1c2a4b0f..2f3d1d0be6 100644 --- a/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts @@ -6,7 +6,6 @@ import { LayersList, DefaultProps, PickingInfo, - FilterContext, } from '@deck.gl/core' import { TileLayer, TileLayerProps } from '@deck.gl/geo-layers' // import { CollisionFilterExtension } from '@deck.gl/extensions' @@ -370,6 +369,7 @@ export class FourwingsClustersLayer extends CompositeLayer< stroked: true, getLineColor: DEFAULT_LINE_COLOR, lineWidthMinPixels: 0.2, + lineWidthUnits: 'pixels', pickable: true, updateTriggers: { getRadius: [radiusScale], From 7efa9997598d9e6ab1238945e79df5e8e81aba23 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 21 Oct 2024 20:40:20 +0200 Subject: [PATCH 2/6] centralize isCluster check --- .../features/map/map-interaction.utils.ts | 4 ++++ .../features/map/map-interactions.hooks.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/fishing-map/features/map/map-interaction.utils.ts b/apps/fishing-map/features/map/map-interaction.utils.ts index 62fb0d4c55..72f79909ed 100644 --- a/apps/fishing-map/features/map/map-interaction.utils.ts +++ b/apps/fishing-map/features/map/map-interaction.utils.ts @@ -5,6 +5,7 @@ import { DeckLayerPickingObject, FourwingsPositionsPickingObject, VesselEventPickingObject, + FourwingsClusterPickingObject, } from '@globalfishingwatch/deck-layers' import { TrackCategory } from 'features/app/analytics.hooks' import { SliceExtendedFourwingsPickingObject } from './map.slice' @@ -13,6 +14,9 @@ export const isTilesClusterLayer = (pickingObject: DeckLayerPickingObject) => pickingObject.subcategory === DataviewType.TileCluster || pickingObject.subcategory === DataviewType.FourwingsTileCluster +export const isTilesClusterLayerCluster = (pickingObject: FourwingsClusterPickingObject) => + pickingObject?.properties?.value > 1 && pickingObject?.properties?.cluster_id !== undefined + export const isRulerLayerPoint = (pickingObject: DeckLayerPickingObject) => pickingObject.category === 'rulers' diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 0adfa8dd8e..fc1c5c734c 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -32,7 +32,12 @@ import { ENCOUNTER_EVENTS_SOURCES } from 'features/dataviews/dataviews.utils' import { selectEventsDataviews } from 'features/dataviews/selectors/dataviews.categories.selectors' import { setHighlightedEvents } from 'features/timebar/timebar.slice' import { useMapRulersDrag } from './overlays/rulers/rulers-drag.hooks' -import { getAnalyticsEvent, isRulerLayerPoint, isTilesClusterLayer } from './map-interaction.utils' +import { + getAnalyticsEvent, + isRulerLayerPoint, + isTilesClusterLayer, + isTilesClusterLayerCluster, +} from './map-interaction.utils' import { SliceExtendedClusterPickingObject, SliceInteractionEvent, @@ -143,7 +148,7 @@ export const useClickedEventConnect = () => { (f) => (f as FourwingsClusterPickingObject).category === DataviewCategory.Events ) as FourwingsClusterPickingObject - if (clusterFeature?.properties?.value > 1) { + if (isTilesClusterLayerCluster(clusterFeature)) { const { expansionZoom } = clusterFeature const { expansionZoom: legacyExpansionZoom } = clusterFeature.properties as any const expansionZoomValue = expansionZoom || legacyExpansionZoom || FOURWINGS_MAX_ZOOM + 0.5 @@ -363,8 +368,8 @@ export const useMapCursor = () => { return 'move' } if (hoverFeatures?.some(isTilesClusterLayer)) { - const isCluster = (hoverFeatures as FourwingsClusterPickingObject[]).some( - (f) => f.properties?.value > 1 + const isCluster = (hoverFeatures as FourwingsClusterPickingObject[]).some((f) => + isTilesClusterLayerCluster(f) ) if (!isCluster) { return 'pointer' From b5809069ccde481799738228978f9d0682d3c1ac Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 21 Oct 2024 20:41:12 +0200 Subject: [PATCH 3/6] export fetch missing datasets thunk --- .../features/datasets/datasets.slice.ts | 39 +++++++++++++++++++ .../vessel-groups-modal.slice.ts | 27 ++++--------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/apps/fishing-map/features/datasets/datasets.slice.ts b/apps/fishing-map/features/datasets/datasets.slice.ts index 92ddeb5afb..7fc76dddb8 100644 --- a/apps/fishing-map/features/datasets/datasets.slice.ts +++ b/apps/fishing-map/features/datasets/datasets.slice.ts @@ -8,6 +8,7 @@ import { APIPagination, Dataset, DatasetsMigration, + DatasetTypes, EndpointId, EndpointParam, UploadResponse, @@ -80,6 +81,44 @@ const parsePOCsDatasets = (dataset: Dataset) => { return dataset } +export const getDatasetByIdsThunk = createAsyncThunk( + 'datasets/getByIds', + async ( + { ids, includeRelated = true }: { ids: string[]; includeRelated?: boolean }, + { rejectWithValue, getState, dispatch } + ) => { + try { + const state = getState() as any + const datasetsToRequest: string[] = [] + let datasets = ids.flatMap((datasetId) => { + const dataset = selectDatasetById(datasetId)(state) + if (!dataset) { + datasetsToRequest.push(datasetId) + } + return (dataset as Dataset) || [] + }) + + if (datasetsToRequest.length) { + const action = await dispatch( + fetchDatasetsByIdsThunk({ ids: datasetsToRequest, includeRelated }) + ) + if (fetchDatasetsByIdsThunk.fulfilled.match(action) && action.payload?.length) { + datasets = datasets.concat( + action.payload.filter((v) => v.type === DatasetTypes.Vessels) as Dataset[] + ) + } + } + return datasets + } catch (e: any) { + console.warn(e) + return rejectWithValue({ + status: parseAPIErrorStatus(e), + message: `${id} - ${parseAPIErrorMessage(e)}`, + }) + } + } +) + export const fetchDatasetByIdThunk = createAsyncThunk< Dataset, string, diff --git a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts index 5f178a2e66..250b1168d8 100644 --- a/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts +++ b/apps/fishing-map/features/vessel-groups/vessel-groups-modal.slice.ts @@ -25,7 +25,7 @@ import { selectVesselsDatasets } from 'features/datasets/datasets.selectors' import { AsyncReducerStatus } from 'utils/async-slice' import { INCLUDES_RELATED_SELF_REPORTED_INFO_ID } from 'features/vessel/vessel.config' import { IdField } from 'features/vessel-groups/vessel-groups.slice' -import { fetchDatasetsByIdsThunk, selectDatasetById } from '../datasets/datasets.slice' +import { getDatasetByIdsThunk } from '../datasets/datasets.slice' import { flatVesselGroupSearchVessels, mergeVesselGroupVesselIdentities, @@ -248,29 +248,16 @@ export const getVesselInVesselGroupThunk = createAsyncThunk( { vesselGroup }: { vesselGroup: VesselGroup }, { signal, rejectWithValue, getState, dispatch } ) => { - const state = getState() as any const datasetIds = uniq(vesselGroup.vessels.flatMap((v) => v.dataset || [])) const updatedDatasetsIds = datasetIds.map(runDatasetMigrations) const hasOutdatedDatasets = difference(datasetIds, updatedDatasetsIds)?.length > 0 - const datasetsToRequest: string[] = [] - let datasets = updatedDatasetsIds.flatMap((datasetId) => { - const dataset = selectDatasetById(datasetId)(state) - if (!dataset) { - datasetsToRequest.push(datasetId) - } - return dataset || [] - }) - - if (datasetsToRequest.length) { - const action = await dispatch( - fetchDatasetsByIdsThunk({ ids: datasetsToRequest, includeRelated: false }) - ) - if (fetchDatasetsByIdsThunk.fulfilled.match(action) && action.payload?.length) { - datasets = datasets.concat( - action.payload.filter((v) => v.type === DatasetTypes.Vessels) as Dataset[] - ) - } + const getDatasetsAction = await dispatch( + getDatasetByIdsThunk({ ids: updatedDatasetsIds, includeRelated: false }) + ) + if (!getDatasetByIdsThunk.fulfilled.match(getDatasetsAction)) { + return rejectWithValue(getDatasetsAction.error) } + const datasets = getDatasetsAction.payload if (!datasets?.length) { return rejectWithValue({ message: 'No datasets found' }) } From a0758545bc0a04fe1ba8f1647d9691db3d647013 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 21 Oct 2024 20:42:48 +0200 Subject: [PATCH 4/6] fetch port event vessels --- apps/fishing-map/features/map/map.slice.ts | 324 ++++++++++-------- .../categories/TileClusterTooltipRow.tsx | 73 +++- libs/api-types/src/fourwings.ts | 10 + libs/api-types/src/index.ts | 1 + .../clusters/fourwings-clusters.types.ts | 1 + 5 files changed, 266 insertions(+), 143 deletions(-) create mode 100644 libs/api-types/src/fourwings.ts diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index 7ff4896177..0e7208c3c1 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -17,6 +17,8 @@ import { EventVessel, EventVesselTypeEnum, APIPagination, + EventTypes, + FourwingsInteraction, } from '@globalfishingwatch/api-types' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' @@ -35,7 +37,11 @@ import { getUTCDate } from '@globalfishingwatch/data-transforms' import { AsyncReducerStatus } from 'utils/async-slice' import { AppDispatch } from 'store' import { selectActiveTemporalgridDataviews } from 'features/dataviews/selectors/dataviews.selectors' -import { fetchDatasetByIdThunk, selectDatasetById } from 'features/datasets/datasets.slice' +import { + fetchDatasetByIdThunk, + getDatasetByIdsThunk, + selectDatasetById, +} from 'features/datasets/datasets.slice' import { getRelatedDatasetByType, getRelatedDatasetsByType, @@ -66,7 +72,14 @@ export type ExtendedFeatureVessel = ExtendedFeatureVesselDatasets & { export type ExtendedEventVessel = EventVessel & { dataset?: string } -export type ExtendedFeatureEvent = ApiEvent & { dataset: Dataset } +export type ExtendedFeatureSingleEvent = ApiEvent & { dataset: Dataset } +export type ExtendedFeatureByVesselEvent = { + id: string + type: EventTypes + vessels: IdentityVessel[] + dataset: Dataset +} +export type ExtendedFeatureEvent = ExtendedFeatureSingleEvent | ExtendedFeatureByVesselEvent export type SliceExtendedFourwingsDeckSublayer = FourwingsDeckSublayer & { vessels?: ExtendedFeatureVessel[] @@ -78,9 +91,10 @@ export type SliceExtendedFourwingsPickingObject = Omit< sublayers: SliceExtendedFourwingsDeckSublayer[] } -export type SliceExtendedClusterPickingObject = FourwingsClusterPickingObject & { - event: ExtendedFeatureEvent -} +export type SliceExtendedClusterPickingObject = + FourwingsClusterPickingObject & { + event: Event + } type SliceExtendedFeature = | SliceExtendedFourwingsPickingObject @@ -371,146 +385,184 @@ export const fetchHeatmapInteractionThunk = createAsyncThunk< } ) -export const fetchClusterEventThunk = createAsyncThunk< - ExtendedFeatureEvent | undefined, - FourwingsClusterPickingObject, - { - dispatch: AppDispatch - } ->('map/fetchEncounterEvent', async (eventFeature, { signal, getState }) => { - const state = getState() as RootState - const eventDataviews = selectEventsDataviews(state) || [] - const dataview = eventDataviews.find((d) => d.id === eventFeature.layerId) - const eventsDataset = dataview?.datasets?.find((d) => d.type === DatasetTypes.Events) - let interactionId = eventFeature.id - let eventId: string | undefined = eventFeature.eventId - if (!eventId && interactionId && eventsDataset) { - const start = getUTCDate(eventFeature?.startTime).toISOString() - const end = getUTCDate(eventFeature?.endTime).toISOString() - const datasetConfig: DataviewDatasetConfig = { - datasetId: eventsDataset?.id, - endpoint: EndpointId.ClusterTilesInteraction, - params: [ - { id: 'z', value: eventFeature.properties.tile.z }, - { id: 'x', value: eventFeature.properties.tile.x }, - { id: 'y', value: eventFeature.properties.tile.y }, - { id: 'rows', value: eventFeature.properties.row as number }, - { id: 'cols', value: eventFeature.properties.col as number }, - ], - query: [ - { id: 'date-range', value: [start, end].join(',') }, - { - id: 'datasets', - value: [eventsDataset.id], - }, - ], - } - if (dataview) { - const filters = getDataviewSqlFiltersResolved(dataview) - datasetConfig.query?.push({ id: 'filters', value: filters }) - const vesselGroups = getVesselGroupInDataview(dataview!) - if (vesselGroups?.length) { - datasetConfig.query?.push({ id: 'vessel-groups', value: vesselGroups }) +export const fetchClusterEventThunk = createAsyncThunk( + 'map/fetchEncounterEvent', + async ( + eventFeature: FourwingsClusterPickingObject, + { signal, getState, dispatch, rejectWithValue } + ) => { + const state = getState() as RootState + const guestUser = selectIsGuestUser(state) + const eventDataviews = selectEventsDataviews(state) || [] + const dataview = eventDataviews.find((d) => d.id === eventFeature.layerId) + const eventsDataset = dataview?.datasets?.find((d) => d.type === DatasetTypes.Events) + const groupBy = + eventFeature.category === 'events' && eventFeature.eventType === EventTypes.Port + ? 'vesselId' + : 'id' + let interactionId = eventFeature.id + let interactionResponse: FourwingsInteraction[] | undefined + let eventId: string | undefined = eventFeature.eventId + if (!eventId && interactionId && eventsDataset) { + const start = getUTCDate(eventFeature?.startTime).toISOString() + const end = getUTCDate(eventFeature?.endTime).toISOString() + const datasetConfig: DataviewDatasetConfig = { + datasetId: eventsDataset?.id, + endpoint: EndpointId.ClusterTilesInteraction, + params: [ + { id: 'z', value: eventFeature.properties.tile.z }, + { id: 'x', value: eventFeature.properties.tile.x }, + { id: 'y', value: eventFeature.properties.tile.y }, + { id: 'rows', value: eventFeature.properties.row as number }, + { id: 'cols', value: eventFeature.properties.col as number }, + ], + query: [ + { id: 'date-range', value: [start, end].join(',') }, + { id: 'group-by', value: groupBy }, + { + id: 'datasets', + value: [eventsDataset.id], + }, + ], + } + if (dataview) { + const filters = getDataviewSqlFiltersResolved(dataview) + if (filters) { + datasetConfig.query?.push({ id: 'filters', value: filters }) + } + const vesselGroups = getVesselGroupInDataview(dataview!) + if (vesselGroups?.length) { + datasetConfig.query?.push({ id: 'vessel-groups', value: vesselGroups }) + } + } + const interactionUrl = resolveEndpoint(eventsDataset, datasetConfig) + if (interactionUrl) { + const response = await GFWAPI.fetch>(interactionUrl, { + signal, + }) + interactionResponse = response.entries[0] + // TODO:deck remove this hardcoded id once the api responds + eventId = response.entries[0][0].id } } - const interactionUrl = resolveEndpoint(eventsDataset, datasetConfig) - if (interactionUrl) { - const eventsIds = await GFWAPI.fetch>(interactionUrl, { - signal, - }) - // TODO:deck remove this hardcoded id once the api responds - eventId = eventsIds.entries[0][0].id - } - } - // TODO:deck get the event dataset from related - if (eventsDataset && eventId) { - const datasetConfig = { - datasetId: eventsDataset.id, - endpoint: EndpointId.EventsDetail, - params: [{ id: 'eventId', value: eventId }], - query: [{ id: 'dataset', value: eventsDataset.id }], - } - const url = resolveEndpoint(eventsDataset, datasetConfig) - if (url) { - const clusterEvent = await GFWAPI.fetch(url, { signal }) - if (!clusterEvent) { - return + if (groupBy === 'vesselId') { + const infoDatasetIds = getRelatedDatasetsByType( + eventsDataset, + DatasetTypes.Vessels, + !guestUser + )?.map((r) => r.id) as string[] + if (!infoDatasetIds?.length) { + return rejectWithValue( + `No info related datasets found in events datasets: ${JSON.stringify(eventsDataset)}` + ) } - if (clusterEvent.type === 'encounter') { - // Workaround to grab information about each vessel dataset - // will need discuss with API team to scale this for other types - const isACarrierTheMainVessel = clusterEvent.vessel.type === EventVesselTypeEnum.Carrier - const fishingVessel = isACarrierTheMainVessel - ? clusterEvent.encounter?.vessel - : clusterEvent.vessel - const carrierVessel = isACarrierTheMainVessel - ? clusterEvent.vessel - : clusterEvent.encounter?.vessel - let vesselsInfo: IdentityVessel[] = [] - const vesselsDatasets = dataview?.datasets - ?.flatMap((d) => d.relatedDatasets || []) - .filter((d) => d?.type === DatasetTypes.Vessels) - - if (vesselsDatasets?.length && fishingVessel && carrierVessel) { - const vesselDataset = selectDatasetById(vesselsDatasets[0].id)(state) as Dataset - const vesselsDatasetConfig = { - datasetId: vesselDataset.id, - endpoint: EndpointId.VesselList, - params: [], - query: [ - { id: 'ids', value: [fishingVessel.id, carrierVessel.id] }, - { id: 'datasets', value: vesselsDatasets.map((d) => d.id) }, - ], + const getDatasetsAction = await dispatch( + getDatasetByIdsThunk({ ids: infoDatasetIds, includeRelated: false }) + ) + if (!getDatasetByIdsThunk.fulfilled.match(getDatasetsAction)) { + return rejectWithValue(getDatasetsAction.error) + } + const infoDatasets = getDatasetsAction.payload + const vesselIds = interactionResponse!?.map((v) => v.id) + const vesselsInfo = await fetchVesselInfo(infoDatasets, vesselIds, signal) + return { + id: interactionId, + type: EventTypes.Port, + vessels: vesselsInfo, + } as ExtendedFeatureByVesselEvent + } else { + // TODO:deck get the event dataset from related + if (eventsDataset && eventId) { + const datasetConfig = { + datasetId: eventsDataset.id, + endpoint: EndpointId.EventsDetail, + params: [{ id: 'eventId', value: eventId }], + query: [{ id: 'dataset', value: eventsDataset.id }], + dataset: eventsDataset, + } + const url = resolveEndpoint(eventsDataset, datasetConfig) + if (url) { + const clusterEvent = await GFWAPI.fetch(url, { signal }) + if (!clusterEvent) { + return } - const vesselsUrl = resolveEndpoint(vesselDataset, vesselsDatasetConfig) - if (vesselsUrl) { - vesselsInfo = await GFWAPI.fetch>(vesselsUrl, { - signal, - }).then((r) => r.entries) + if (clusterEvent.type === 'encounter') { + // Workaround to grab information about each vessel dataset + // will need discuss with API team to scale this for other types + const isACarrierTheMainVessel = clusterEvent.vessel.type === EventVesselTypeEnum.Carrier + const fishingVessel = isACarrierTheMainVessel + ? clusterEvent.encounter?.vessel + : clusterEvent.vessel + const carrierVessel = isACarrierTheMainVessel + ? clusterEvent.vessel + : clusterEvent.encounter?.vessel + let vesselsInfo: IdentityVessel[] = [] + const vesselsDatasets = dataview?.datasets + ?.flatMap((d) => d.relatedDatasets || []) + .filter((d) => d?.type === DatasetTypes.Vessels) + + if (vesselsDatasets?.length && fishingVessel && carrierVessel) { + const vesselDataset = selectDatasetById(vesselsDatasets[0].id)(state) as Dataset + const vesselsDatasetConfig = { + datasetId: vesselDataset.id, + endpoint: EndpointId.VesselList, + params: [], + query: [ + { id: 'ids', value: [fishingVessel.id, carrierVessel.id] }, + { id: 'datasets', value: vesselsDatasets.map((d) => d.id) }, + ], + } + const vesselsUrl = resolveEndpoint(vesselDataset, vesselsDatasetConfig) + if (vesselsUrl) { + vesselsInfo = await GFWAPI.fetch>(vesselsUrl, { + signal, + }).then((r) => r.entries) + } + } + const fishingVesselDataset = + vesselsInfo.find( + (v) => + getVesselProperty(v, 'id', { + identitySource: VesselIdentitySourceEnum.SelfReported, + }) === fishingVessel?.id + )?.dataset || '' + const carrierVesselDataset = + vesselsInfo.find( + (v) => + getVesselProperty(v, 'id', { + identitySource: VesselIdentitySourceEnum.SelfReported, + }) === carrierVessel?.id + )?.dataset || '' + const carrierExtendedVessel: ExtendedEventVessel = { + ...(carrierVessel as EventVessel), + dataset: carrierVesselDataset, + } + const fishingExtendedVessel: ExtendedEventVessel = { + ...(fishingVessel as EventVessel), + dataset: fishingVesselDataset, + } + return { + ...clusterEvent, + vessel: carrierExtendedVessel, + ...(clusterEvent.encounter && { + encounter: { + ...clusterEvent.encounter, + vessel: fishingExtendedVessel, + }, + }), + dataset: eventsDataset, + } } - } - const fishingVesselDataset = - vesselsInfo.find( - (v) => - getVesselProperty(v, 'id', { - identitySource: VesselIdentitySourceEnum.SelfReported, - }) === fishingVessel?.id - )?.dataset || '' - const carrierVesselDataset = - vesselsInfo.find( - (v) => - getVesselProperty(v, 'id', { - identitySource: VesselIdentitySourceEnum.SelfReported, - }) === carrierVessel?.id - )?.dataset || '' - const carrierExtendedVessel: ExtendedEventVessel = { - ...(carrierVessel as EventVessel), - dataset: carrierVesselDataset, - } - const fishingExtendedVessel: ExtendedEventVessel = { - ...(fishingVessel as EventVessel), - dataset: fishingVesselDataset, - } - return { - ...clusterEvent, - vessel: carrierExtendedVessel, - ...(clusterEvent.encounter && { - encounter: { - ...clusterEvent.encounter, - vessel: fishingExtendedVessel, - }, - }), - dataset: eventsDataset, + return { ...clusterEvent, dataset: eventsDataset } + } else { + console.warn('Missing url for endpoints', eventsDataset, datasetConfig) } } - return { ...clusterEvent, dataset: eventsDataset } - } else { - console.warn('Missing url for endpoints', eventsDataset, datasetConfig) } - } - return -}) + return + } +) type BQClusterEvent = Record export const fetchBQEventThunk = createAsyncThunk< diff --git a/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx b/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx index b47248ea1d..e3c5bc5864 100644 --- a/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx +++ b/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx @@ -22,14 +22,15 @@ import { getEventDescriptionComponent } from 'utils/events' import { useMapViewState } from '../../map-viewport.hooks' import { ExtendedEventVessel, - ExtendedFeatureEvent, + ExtendedFeatureByVesselEvent, + ExtendedFeatureSingleEvent, SliceExtendedClusterPickingObject, } from '../../map.slice' import styles from '../Popup.module.css' const parseEncounterEvent = ( - event: ExtendedFeatureEvent | undefined -): ExtendedFeatureEvent | undefined => { + event: ExtendedFeatureSingleEvent | undefined +): ExtendedFeatureSingleEvent | undefined => { if (!event) return event const carrierVessel: EventVessel = event.vessel.type === 'carrier' ? event.vessel : (event.encounter?.vessel as EventVessel) @@ -48,7 +49,7 @@ const parseEncounterEvent = ( } type EncountersLayerProps = { - feature: SliceExtendedClusterPickingObject + feature: SliceExtendedClusterPickingObject showFeaturesDetails: boolean } @@ -177,6 +178,53 @@ function EncounterTooltipRow({ feature, showFeaturesDetails }: EncountersLayerPr ) } +type PortVisitLayerProps = { + feature: SliceExtendedClusterPickingObject + showFeaturesDetails: boolean +} +function PortVisitEventTooltipRow({ feature, showFeaturesDetails }: PortVisitLayerProps) { + console.log('🚀 ~ PortVisitEventTooltipRow ~ feature:', feature) + return null + // const { t } = useTranslation() + // const { datasetId, event, color } = feature + // const title = getDatasetLabel({ id: datasetId! }) + // const infoDataset = event?.dataset?.relatedDatasets?.find((d) => d.type === DatasetTypes.Vessels) + // return ( + //
+ // + //
+ // {

{title}

} + // {showFeaturesDetails && ( + //
+ // {event?.vessel ? ( + //
+ // + // + // {formatInfoField(event.vessel.name, 'shipname')} + // + // ({formatInfoField(event.vessel.flag, 'flag')}){' '} + // + // {getEventDescriptionComponent(event)?.description} + // + //
+ // ) : ( + // t('event.noData', 'No data available') + // )} + //
+ // )} + //
+ //
+ // ) +} + function ClusterEventTooltipRow({ feature, showFeaturesDetails }: EncountersLayerProps) { const { t } = useTranslation() const { datasetId, event, color } = feature @@ -262,12 +310,23 @@ function TileClusterTooltipRow({ features, showFeaturesDetails }: TileContextLay {features.map((feature, index) => { const key = `${feature.title}-${index}` + const eventFeature = + feature as SliceExtendedClusterPickingObject if (GFW_CLUSTER_LAYERS.some((source) => feature.layerId === source)) { + if (feature.layerId.includes('port')) { + return ( + } + showFeaturesDetails={showFeaturesDetails} + /> + ) + } if (feature.layerId.includes('encounter')) { return ( ) @@ -275,7 +334,7 @@ function TileClusterTooltipRow({ features, showFeaturesDetails }: TileContextLay return ( ) @@ -283,7 +342,7 @@ function TileClusterTooltipRow({ features, showFeaturesDetails }: TileContextLay return ( ) diff --git a/libs/api-types/src/fourwings.ts b/libs/api-types/src/fourwings.ts new file mode 100644 index 0000000000..b700d24e36 --- /dev/null +++ b/libs/api-types/src/fourwings.ts @@ -0,0 +1,10 @@ +export type FourwingsHeatmapInteraction = { + id: string +} + +export type FourwingsEventsInteraction = { + events: number + id: string +} + +export type FourwingsInteraction = FourwingsHeatmapInteraction | FourwingsEventsInteraction diff --git a/libs/api-types/src/index.ts b/libs/api-types/src/index.ts index 88138cd448..291d8c638f 100644 --- a/libs/api-types/src/index.ts +++ b/libs/api-types/src/index.ts @@ -3,6 +3,7 @@ export * from './dataviews' export * from './download' export * from './downloadActivity' export * from './events' +export * from './fourwings' export * from './geometries' export * from './i18n' export * from './identity-vessel' diff --git a/libs/deck-layers/src/layers/fourwings/clusters/fourwings-clusters.types.ts b/libs/deck-layers/src/layers/fourwings/clusters/fourwings-clusters.types.ts index abb5e6078b..cc4e9cf919 100644 --- a/libs/deck-layers/src/layers/fourwings/clusters/fourwings-clusters.types.ts +++ b/libs/deck-layers/src/layers/fourwings/clusters/fourwings-clusters.types.ts @@ -39,6 +39,7 @@ export type FourwingsClusterPickingObject = FourwingsClusterFeature & expansionZoom?: number datasetId?: string eventId?: string + eventType?: FourwingsClusterEventType }> export type FourwingsClusterPickingInfo = PickingInfo< From 6c4f17e8b9861708c2fabf6a5193901e3e8ad2c6 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 21 Oct 2024 21:09:00 +0200 Subject: [PATCH 5/6] render VesselTable for port_visit events --- .../features/map/map-interactions.hooks.ts | 1 + apps/fishing-map/features/map/map.slice.ts | 30 +++++++-- .../categories/TileClusterTooltipRow.tsx | 64 +++++++------------ .../map/popups/categories/VesselsTable.tsx | 4 +- 4 files changed, 50 insertions(+), 49 deletions(-) diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index fc1c5c734c..8942100185 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -57,6 +57,7 @@ export const SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION = [ DataviewCategory.Activity, DataviewCategory.Detections, DataviewCategory.VesselGroups, + DataviewCategory.Events, ] const useMapClusterTilesLoading = () => { diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index 0e7208c3c1..1f0c0836a2 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -19,6 +19,7 @@ import { APIPagination, EventTypes, FourwingsInteraction, + FourwingsEventsInteraction, } from '@globalfishingwatch/api-types' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' @@ -66,7 +67,7 @@ type ExtendedFeatureVesselDatasets = IdentityVessel & { // TODO extract this type in app types export type ExtendedFeatureVessel = ExtendedFeatureVesselDatasets & { - hours: number + hours?: number [key: string]: any } @@ -76,7 +77,7 @@ export type ExtendedFeatureSingleEvent = ApiEvent & { dataset: Data export type ExtendedFeatureByVesselEvent = { id: string type: EventTypes - vessels: IdentityVessel[] + vessels: ExtendedFeatureVessel[] dataset: Dataset } export type ExtendedFeatureEvent = ExtendedFeatureSingleEvent | ExtendedFeatureByVesselEvent @@ -241,7 +242,7 @@ const fetchVesselInfo = async (datasets: Dataset[], vesselIds: string[], signal: } } -export type ActivityProperty = 'hours' | 'detections' +export type ActivityProperty = 'hours' | 'detections' | 'events' export const fetchHeatmapInteractionThunk = createAsyncThunk< { vessels: SublayerVessels[] } | undefined, { @@ -462,13 +463,30 @@ export const fetchClusterEventThunk = createAsyncThunk( if (!getDatasetByIdsThunk.fulfilled.match(getDatasetsAction)) { return rejectWithValue(getDatasetsAction.error) } - const infoDatasets = getDatasetsAction.payload - const vesselIds = interactionResponse!?.map((v) => v.id) + const infoDatasets = getDatasetsAction.payload.flatMap((v) => v) + const vesselIds = (interactionResponse as FourwingsEventsInteraction[])! + ?.sort((a, b) => b.events - a.events) + .slice(0, MAX_TOOLTIP_LIST) + .map((v) => v.id) const vesselsInfo = await fetchVesselInfo(infoDatasets, vesselIds, signal) + const vessels = (interactionResponse as FourwingsEventsInteraction[])!.flatMap( + (interaction) => { + const vesselInfo = vesselsInfo?.find((vesselInfo) => { + const vesselInfoIds = vesselInfo.selfReportedInfo?.map((s) => s.id) + return vesselInfoIds.includes(interaction.id) + }) + return { + id: interaction.id, + ...vesselInfo, + dataset: infoDatasets[0] as any, + events: interaction.events, + } as ExtendedFeatureVessel + } + ) return { id: interactionId, type: EventTypes.Port, - vessels: vesselsInfo, + vessels, } as ExtendedFeatureByVesselEvent } else { // TODO:deck get the event dataset from related diff --git a/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx b/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx index e3c5bc5864..a956a83079 100644 --- a/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx +++ b/apps/fishing-map/features/map/popups/categories/TileClusterTooltipRow.tsx @@ -2,7 +2,7 @@ import { Fragment, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { stringify } from 'qs' import { Button, Icon } from '@globalfishingwatch/ui-components' -import { DatasetTypes, EventVessel } from '@globalfishingwatch/api-types' +import { DatasetTypes, DataviewCategory, EventVessel } from '@globalfishingwatch/api-types' import { AsyncReducerStatus } from 'utils/async-slice' import I18nDate from 'features/i18n/i18nDate' import { @@ -27,6 +27,7 @@ import { SliceExtendedClusterPickingObject, } from '../../map.slice' import styles from '../Popup.module.css' +import VesselsTable from './VesselsTable' const parseEncounterEvent = ( event: ExtendedFeatureSingleEvent | undefined @@ -183,46 +184,27 @@ type PortVisitLayerProps = { showFeaturesDetails: boolean } function PortVisitEventTooltipRow({ feature, showFeaturesDetails }: PortVisitLayerProps) { - console.log('🚀 ~ PortVisitEventTooltipRow ~ feature:', feature) - return null - // const { t } = useTranslation() - // const { datasetId, event, color } = feature - // const title = getDatasetLabel({ id: datasetId! }) - // const infoDataset = event?.dataset?.relatedDatasets?.find((d) => d.type === DatasetTypes.Vessels) - // return ( - //
- // - //
- // {

{title}

} - // {showFeaturesDetails && ( - //
- // {event?.vessel ? ( - //
- // - // - // {formatInfoField(event.vessel.name, 'shipname')} - // - // ({formatInfoField(event.vessel.flag, 'flag')}){' '} - // - // {getEventDescriptionComponent(event)?.description} - // - //
- // ) : ( - // t('event.noData', 'No data available') - // )} - //
- // )} - //
- //
- // ) + const { datasetId, event, color } = feature + const title = getDatasetLabel({ id: datasetId! }) + return ( +
+ +
+ {

{title}

} + {showFeaturesDetails && ( + + )} +
+
+ ) } function ClusterEventTooltipRow({ feature, showFeaturesDetails }: EncountersLayerProps) { diff --git a/apps/fishing-map/features/map/popups/categories/VesselsTable.tsx b/apps/fishing-map/features/map/popups/categories/VesselsTable.tsx index 0b4d636569..b1d7f2fde6 100644 --- a/apps/fishing-map/features/map/popups/categories/VesselsTable.tsx +++ b/apps/fishing-map/features/map/popups/categories/VesselsTable.tsx @@ -147,7 +147,7 @@ function VesselsTable({ return hasDatasets }) - const isHoursProperty = vesselProperty !== 'detections' + const isHoursProperty = vesselProperty !== 'detections' && vesselProperty !== 'events' const isPresenceActivity = activityType === DatasetSubCategory.Presence return ( @@ -161,7 +161,7 @@ function VesselsTable({ {isPresenceActivity ? t('vessel.type', 'Type') : t('vessel.gearType_short', 'Gear')} {/* Disabled for detections to allocate some space for timestamps interaction */} - {vesselProperty !== 'detections' && {t('vessel.source_short', 'source')}} + {isHoursProperty && {t('vessel.source_short', 'source')}} {showValue && ( {feature?.unit === 'hours' && t('common.hour_other', 'hours')} From f5607b8cb5ad8071506ff69157b3b392d9aa23cb Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 21 Oct 2024 21:21:04 +0200 Subject: [PATCH 6/6] fix port_visit VesselPin --- apps/fishing-map/features/map/map.slice.ts | 11 +++++++++++ apps/fishing-map/features/vessel/VesselPin.tsx | 5 ++++- .../fourwings/clusters/FourwingsClustersLayer.ts | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index 1f0c0836a2..69ddd31cac 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -475,10 +475,21 @@ export const fetchClusterEventThunk = createAsyncThunk( const vesselInfoIds = vesselInfo.selfReportedInfo?.map((s) => s.id) return vesselInfoIds.includes(interaction.id) }) + const infoDataset = selectDatasetById(vesselInfo?.dataset as string)(state) + const trackFromRelatedDataset = infoDataset || vesselInfo?.dataset + const trackDatasetId = getRelatedDatasetByType( + trackFromRelatedDataset, + DatasetTypes.Tracks, + { fullDatasetAllowed: !guestUser } + )?.id + const trackDataset = selectDatasetById(trackDatasetId as string)(state) + return { id: interaction.id, ...vesselInfo, dataset: infoDatasets[0] as any, + infoDataset, + trackDataset, events: interaction.events, } as ExtendedFeatureVessel } diff --git a/apps/fishing-map/features/vessel/VesselPin.tsx b/apps/fishing-map/features/vessel/VesselPin.tsx index 61fc573f4b..d11f439e94 100644 --- a/apps/fishing-map/features/vessel/VesselPin.tsx +++ b/apps/fishing-map/features/vessel/VesselPin.tsx @@ -68,7 +68,10 @@ function VesselPin({ const dispatch = useAppDispatch() const { upsertDataviewInstance, deleteDataviewInstance } = useDataviewInstancesConnect() const vesselsInWorkspace = useSelector(selectTrackDataviews) - const infoDatasetId = vessel?.dataset || vesselToResolve?.datasetId || '' + const infoDatasetId = + typeof vessel?.dataset === 'string' + ? vessel?.dataset + : (vessel?.dataset as any)?.id || vesselToResolve?.datasetId || '' const infoDataset = useSelector(selectDatasetById(infoDatasetId)) const vesselInWorkspace = getVesselInWorkspace( vesselsInWorkspace, diff --git a/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts b/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts index 2f3d1d0be6..0a9b806af0 100644 --- a/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/clusters/FourwingsClustersLayer.ts @@ -158,6 +158,7 @@ export class FourwingsClustersLayer extends CompositeLayer< subcategory: this.props.subcategory, startTime: this.props.startTime, endTime: this.props.endTime, + eventType: this.props.eventType, uniqueFeatureInteraction: true, expansionZoom, }