From 19af61876f14566c00be4f5de52840879ad03270 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Mon, 25 Mar 2024 13:26:30 +0100 Subject: [PATCH] add cluster layer --- apps/fishing-map/public/events-sprite.png | Bin 0 -> 1051 bytes .../src/resolvers/clusters.ts | 23 ++++++ .../src/resolvers/index.ts | 8 ++ .../src/resolvers/types.ts | 4 +- libs/deck-layers/src/index.ts | 13 +-- .../src/layers/cluster/ClusterLayer.ts | 76 ++++++++++++++++++ 6 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 apps/fishing-map/public/events-sprite.png create mode 100644 libs/deck-layer-composer/src/resolvers/clusters.ts create mode 100644 libs/deck-layers/src/layers/cluster/ClusterLayer.ts diff --git a/apps/fishing-map/public/events-sprite.png b/apps/fishing-map/public/events-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..47b3d6013e30f18918c4f639b7b5010daeec1796 GIT binary patch literal 1051 zcmV+$1mydPP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91bf5zO1ONa40RR91Bme*a0ALv(bpQYZg-Jv~RCodHTRV=_KoH%93oz+Q za1r8-AR&jl)DiXya0WOK!z}3tP`~?$_Z{SAlHy*kgGv3T>Kd18^&A62gpyz7X04gyhmRFDIOGs^Imv` z#GfGdkif?pX-gmamcq)>dcX8I&VGVdO_;itc#dSBA(}_-nlTtF7rA-7?Q{8$_JUcA z9QVBO7zu75F%G4A#$@bNl;-jBOXWSW6Ra#kwDH0d1pWxIIF#-)Hglw)G>4a83g?N9 zV3kUSwza`Re}SZOD79mbl%|%(_wviCw-gkTKDEG8q32(wE;4gjr(f;Ud!1Zsfn_OK z`qsb=f|PP7^<&O8l*aY)TjM(J31(XfFvb?}3Un)nQvbE9-_n>~erAoOVj&r;6g9q@ zlopY>V>TiJj&~uf_~ftz z%dhqR636o`JZ~bhn0c-|L3I!D=3~Vt2XfJ{{95m8eodMg4pL^>=ZZH_1^eD3xoGs} zr1MYs30B&`d(E6{c!y1I4yp0UMI+U>mEFisFl&`m=V9&`?y#Zld#%YuqZgN0-ZMYJ zYs{dPo*b6&70Ol|tUTnRVfnP)Z~B>O9h~TDVf_nY`L)_77mZfAlkz;$>~^^3f$Rl) z;<8#3!EVw!z|oEzmT;VM^LjJZP%!J&ROXK14ja9A>Ad8kp>2AvC;SAf)`XaI4R5j8 zno}$vxoE^}d+9a5rWZ*v%RX1^>BhqAIlbMy*}@xWf#Tn%_kw}fD2tir%H!5w-Wh6M z!9gP!dp9$XJ8sUW_afA6aTI84H61<3uM|)gr-Ir8xe< zoz{n>`U)fW&rxuRKIPLmp*IM9_ z+Em)s1|NPt9<+}4ps3ygFRQ7v@dBIb`M~vm^;3w@B*7%sRPK3$r(ciHRK~QPmVbC` zu^RN!KjfA@7kFsm)U^b!ertTCVGPE~g+Bh?>sTLp4S5Q=8U(}mll { + 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], + }) + } +}