diff --git a/apps/fishing-map/public/events-sprite.png b/apps/fishing-map/public/events-sprite.png new file mode 100644 index 0000000000..47b3d6013e Binary files /dev/null and b/apps/fishing-map/public/events-sprite.png differ diff --git a/libs/deck-layer-composer/src/resolvers/clusters.ts b/libs/deck-layer-composer/src/resolvers/clusters.ts new file mode 100644 index 0000000000..194fa725c8 --- /dev/null +++ b/libs/deck-layer-composer/src/resolvers/clusters.ts @@ -0,0 +1,23 @@ +import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' +import { ClusterLayerProps } from '@globalfishingwatch/deck-layers' +import { resolveEndpoint } from '@globalfishingwatch/datasets-client' +import { ResolverGlobalConfig } from './types' + +// TODO: decide if include static here or create a new one +export const resolveDeckClusterLayerProps = ( + dataview: UrlDataviewInstance, + { start, end }: ResolverGlobalConfig +): ClusterLayerProps => { + const dataset = dataview.datasets?.[0] + const tilesUrl = dataset ? resolveEndpoint(dataset, dataview.datasetsConfig?.[0]!) : undefined + + return { + id: dataview.id, + datasetId: dataset?.id || '', + color: dataview.config?.color || '', + start: start, + end: end, + visible: dataview.config?.visible ?? true, + tilesUrl: tilesUrl || '', + } +} diff --git a/libs/deck-layer-composer/src/resolvers/index.ts b/libs/deck-layer-composer/src/resolvers/index.ts index 7fe39bce33..7e1a62df44 100644 --- a/libs/deck-layer-composer/src/resolvers/index.ts +++ b/libs/deck-layer-composer/src/resolvers/index.ts @@ -3,6 +3,7 @@ import { DataviewType, DataviewInstance } from '@globalfishingwatch/api-types' import { AnyDeckLayer, BaseMapLayer, + ClusterLayer, ContextLayer, EEZLayer, FourwingsLayer, @@ -12,10 +13,12 @@ import { ResolverGlobalConfig } from './types' import { resolveDeckBasemapLayerProps } from './basemap' import { resolveDeckFourwingsLayerProps } from './fourwings' import { resolveDeckContextLayerProps, resolveDeckEEZLayerProps } from './context' +import { resolveDeckClusterLayerProps } from './clusters' import { resolveDeckVesselLayerProps } from './vessels' export * from './basemap' export * from './context' +export * from './clusters' export * from './dataviews' export * from './fourwings' export * from './types' @@ -48,6 +51,11 @@ export const dataviewToDeckLayer = ( const layer = new ContextLayer(deckLayerProps) return layer } + if (dataview.config?.type === DataviewType.TileCluster) { + const deckLayerProps = resolveDeckClusterLayerProps(dataview, globalConfig) + const layer = new ClusterLayer(deckLayerProps) + return layer + } if (dataview.config?.type === DataviewType.Track) { const deckLayerProps = resolveDeckVesselLayerProps(dataview, globalConfig, interactions) const layer = new VesselLayer(deckLayerProps) diff --git a/libs/deck-layer-composer/src/resolvers/types.ts b/libs/deck-layer-composer/src/resolvers/types.ts index 644bb7afb7..1ee4ed8e20 100644 --- a/libs/deck-layer-composer/src/resolvers/types.ts +++ b/libs/deck-layer-composer/src/resolvers/types.ts @@ -2,8 +2,8 @@ import { EventTypes } from '@globalfishingwatch/api-types' import { FourwingsResolution, FourwingsVisualizationMode } from '@globalfishingwatch/deck-layers' export type ResolverGlobalConfig = { - start?: string - end?: string + start: string + end: string zoom?: number token?: string bivariateDataviews?: [string, string] diff --git a/libs/deck-layers/src/index.ts b/libs/deck-layers/src/index.ts index 91348badc7..922913e110 100644 --- a/libs/deck-layers/src/index.ts +++ b/libs/deck-layers/src/index.ts @@ -1,12 +1,13 @@ export * from './layers/basemap/BasemapLayer' +export * from './layers/cluster/ClusterLayer' +export * from './layers/context/context.config' export * from './layers/context/ContextLayer' export * from './layers/context/EEZLayer' -export * from './layers/context/context.config' -export * from './layers/fourwings/FourwingsLayer' -export * from './layers/fourwings/fourwings.types' -export * from './layers/vessel/VesselLayer' -export * from './layers/rulers/RulersLayer' export * from './layers/fourwings/fourwings.config' +export * from './layers/fourwings/fourwings.types' export * from './layers/fourwings/fourwings.utils' -export * from './utils' +export * from './layers/fourwings/FourwingsLayer' +export * from './layers/rulers/RulersLayer' +export * from './layers/vessel/VesselLayer' export * from './types' +export * from './utils' diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts new file mode 100644 index 0000000000..09f66ce83e --- /dev/null +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -0,0 +1,76 @@ +import { CompositeLayer, DefaultProps, LayerProps } from '@deck.gl/core' +import { MVTLayer, TileLayerProps } from '@deck.gl/geo-layers' +import { Feature, Point } from 'geojson' +import { stringify } from 'qs' +import { GFWAPI } from '@globalfishingwatch/api-client' +import { LayerGroup, getLayerGroupOffset, hexToDeckColor } from '../../utils' + +type EventType = 'encounter' | 'gap' | 'port_visit' + +export type ClusterLayerProps = { + color: string + datasetId: string + end: string + eventType?: EventType + id: string + maxClusterZoom?: number + start: string + tilesUrl: string + visible: boolean +} + +type ClusterFeatureProps = { + count: number + event_id: string + expansionZoom: number +} + +type ClusterFeature = Feature + +const defaultProps: DefaultProps = { + eventType: 'encounter', + maxClusterZoom: 4, +} + +const ICON_MAPPING: Record = { + encounter: { x: 0, y: 0, width: 36, height: 36, mask: true }, + gap: { x: 40, y: 0, width: 36, height: 36, mask: true }, + port_visit: { x: 80, y: 0, width: 36, height: 36, mask: true }, +} + +export class ClusterLayer extends CompositeLayer { + static layerName = 'ClusterLayer' + static defaultProps = defaultProps + + renderLayers() { + const baseUrl = GFWAPI.generateUrl(this.props.tilesUrl as string, { absolute: true }) + const params = { + 'date-range': [this.props.start, this.props.end], + 'max-cluster-zoom': this.props.maxClusterZoom, + } + const url = `${baseUrl}&${stringify(params, { arrayFormat: 'indices' })}` + const color = hexToDeckColor(this.props.color) + + return new MVTLayer>({ + data: url, + maxRequests: 100, + debounceTime: 500, + getPolygonOffset: (params: any) => getLayerGroupOffset(LayerGroup.Cluster, params), + getFillColor: color, + getIconColor: color, + getPointRadius: (d: any) => + d.properties.count > 1 ? 11 + Math.sqrt(d.properties.count) / 3 : 0, + iconAtlas: '/events-sprite.png', + iconMapping: ICON_MAPPING, + getIcon: (d: ClusterFeature) => d.properties.count === 1 && this.props.eventType, + getIconSize: 16, + pointRadiusMinPixels: 0, + pointRadiusMaxPixels: 40, + pointType: 'circle+icon+text', + pointRadiusUnits: 'pixels', + getText: (d: ClusterFeature) => d.properties.count > 1 && d.properties.count.toString(), + getTextSize: 12, + getTextColor: [22, 63, 137], + }) + } +}