Skip to content

Commit 18f3398

Browse files
authored
Merge pull request #327 from debrief/326-layers-visible-toggle
326 Allow layers to be filtered for visible features
2 parents ab99b72 + 08b3032 commit 18f3398

22 files changed

Lines changed: 841 additions & 6412 deletions

.windsurfrules

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This project is being maintained in this repository: https://github.com/debrief/reactol

src/components/ControlPanel/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ const ControlPanel: React.FC<TimeProps> = ({ bounds, handleSave, isDirty }) => {
8686
>
8787
<Button
8888
style={buttonStyle}
89-
color='primary'
9089
variant={viewportFrozen ? 'solid' : 'outlined'}
9190
onClick={toggleFreezeViewport}
9291
>

src/components/Layers/LayersToolbar.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
ShrinkOutlined,
55
CloseCircleOutlined,
66
DeleteOutlined,
7-
LineChartOutlined
7+
FilterFilled,
8+
FilterOutlined
89
} from '@ant-design/icons'
910
import { ToolButton } from './ToolButton'
1011
import { CopyButton } from './CopyButton'
@@ -18,16 +19,18 @@ interface LayersToolbarProps {
1819
isExpanded: boolean
1920
hasSelection: boolean
2021
hasTemporalFeature: boolean
22+
hasTimeFilter: boolean
23+
onFilterForTime: (value: boolean) => void
2124
}
2225

2326
export const LayersToolbar: React.FC<LayersToolbarProps> = ({
2427
onCollapse,
2528
onClearSelection,
2629
onDelete,
27-
onGraph,
2830
isExpanded,
2931
hasSelection,
30-
hasTemporalFeature
32+
hasTimeFilter,
33+
onFilterForTime
3134
}) => {
3235
return (
3336
<div style={{ position: 'relative' }}>
@@ -76,6 +79,18 @@ export const LayersToolbar: React.FC<LayersToolbarProps> = ({
7679
<CopyButton />
7780
<PasteButton />
7881
<ToolButton
82+
onClick={() => onFilterForTime(!hasTimeFilter)}
83+
className='layers-delete-button'
84+
disabled={false}
85+
filled={hasTimeFilter}
86+
icon={hasTimeFilter ? <FilterFilled /> : <FilterOutlined />}
87+
title={
88+
hasTimeFilter
89+
? 'Cancel filter features by time'
90+
: 'Filter features by time'
91+
}
92+
/>
93+
{/* <ToolButton
7994
onClick={onGraph}
8095
disabled={!hasTemporalFeature}
8196
icon={<LineChartOutlined />}
@@ -84,7 +99,7 @@ export const LayersToolbar: React.FC<LayersToolbarProps> = ({
8499
? 'View graph of selected features'
85100
: 'Select a time-related feature to enable graphs'
86101
}
87-
/>
102+
/> */}
88103
</Button.Group>
89104
</Flex>
90105
</div>

src/components/Layers/ToolButton.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ interface ToolProps {
77
title: string
88
disabled: boolean
99
className?: string
10+
filled?: boolean
1011
}
1112

1213
export const ToolButton: React.FC<ToolProps> = ({
1314
onClick,
1415
icon,
1516
title,
1617
disabled,
17-
className
18+
className,
19+
filled
1820
}) => {
1921
return (
2022
<Tooltip title={title}>
@@ -23,7 +25,7 @@ export const ToolButton: React.FC<ToolProps> = ({
2325
className={className}
2426
onClick={onClick}
2527
disabled={disabled}
26-
type='primary'
28+
type={filled !== undefined ? (filled ? 'primary' : 'default') : 'primary'}
2729
icon={icon}
2830
/>
2931
</Tooltip>
Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import React from 'react'
21
import { Feature } from 'geojson'
32
import { TreeDataNode } from 'antd'
4-
import { FolderOutlined, PlusCircleOutlined } from '@ant-design/icons'
5-
import { Tooltip } from 'antd'
6-
import { FeatureIcon } from './FeatureIcon'
73
import { symbolOptions } from '../../helpers/symbolTypes'
8-
import { EnvOptions } from '../../types'
4+
import { EnvOptions, FeatureTypes } from '../../types'
5+
import { featureIsVisibleInPeriod } from '../../helpers/featureIsVisibleAtTime'
6+
import { BUOY_FIELD_TYPE, ZONE_TYPE, REFERENCE_POINT_TYPE, BACKDROP_TYPE } from '../../constants'
97

108
// Import node constants from the constants file
119
import {
@@ -22,8 +20,20 @@ type FieldDataNode = {
2220
children: FieldDataNode[]
2321
}
2422

23+
// Import React for type definitions
24+
import React from 'react'
25+
26+
// Using React.MouseEvent for the event type
2527
export type HandleAddFunction = (e: React.MouseEvent, key: string, title: string) => void
2628

29+
// Interface for icon creators to be passed from Layers component
30+
export interface IconCreators {
31+
createFolderIcon: () => React.ReactNode
32+
createFeatureIcon: (dataType: string, color?: string, environment?: string) => React.ReactNode
33+
createAddIcon: (key: string, title: string, handleAdd: HandleAddFunction) => React.ReactNode
34+
createTitleElement: (title: string) => React.ReactNode
35+
}
36+
2737
export class TreeDataBuilder {
2838
/**
2939
* Get the name for a feature
@@ -88,74 +98,78 @@ export class TreeDataBuilder {
8898
}
8999

90100
/**
91-
* Get the icon for a node
101+
* Get the appropriate icon for a node using the provided icon creators
92102
* @param feature The feature (if any)
93103
* @param key The node key
94104
* @param title The node title
95105
* @param handleAdd The add handler function
106+
* @param iconCreators The icon creator functions
96107
* @param button Optional custom button
97108
* @returns The icon React node
98109
*/
99110
static getIcon(
100111
feature: Feature | undefined,
101112
key: string,
102113
title: string,
103-
handleAdd?: HandleAddFunction,
114+
handleAdd: HandleAddFunction | undefined,
115+
iconCreators: IconCreators,
104116
button?: React.ReactNode
105117
): React.ReactNode {
106118
// If no feature is provided, this is a parent node - show plus icon
107119
if (!feature) {
108120
if (!handleAdd) return null
109-
110121
if (button) return button
111-
112-
return (
113-
<Tooltip title={this.addIconLabelFor(key, title)}>
114-
<PlusCircleOutlined
115-
className="add-icon"
116-
style={{ cursor: 'copy' }}
117-
onClick={(e: React.MouseEvent) => handleAdd(e, key, title)}
118-
/>
119-
</Tooltip>
120-
)
122+
return iconCreators.createAddIcon(key, title, handleAdd)
121123
}
122124

123125
// For leaf nodes, show type-specific icon based on dataType
124126
const dataType = feature.properties?.dataType
125127
const color = feature.properties?.stroke || feature.properties?.color || feature.properties?.['marker-color']
126128
const environment = feature.properties?.env
127-
return dataType ? <FeatureIcon dataType={dataType} color={color} environment={environment} /> : <FolderOutlined />
129+
return dataType
130+
? iconCreators.createFeatureIcon(dataType, color, environment)
131+
: iconCreators.createFolderIcon()
128132
}
129133

130134
/**
131135
* Build a track node
132136
* @param features The features to include
133137
* @param handleAdd The add handler function
138+
* @param iconCreators The icon creator functions
139+
* @param useTimeFilter Whether to filter by time
134140
* @returns A TreeDataNode for tracks
135141
*/
136-
static buildTrackNode(features: Feature[], handleAdd: HandleAddFunction): TreeDataNode {
142+
static buildTrackNode(
143+
features: Feature[],
144+
handleAdd: HandleAddFunction,
145+
iconCreators: IconCreators,
146+
useTimeFilter: boolean
147+
): TreeDataNode {
137148
// generate new root
138149
const root: TreeDataNode = {
139150
title: 'Units',
140151
key: NODE_TRACKS,
141-
icon: <FolderOutlined />,
152+
icon: iconCreators.createFolderIcon(),
142153
children: [],
143154
}
144155
const environments = symbolOptions.map((env): TreeDataNode => ({
145156
title: env.label,
146157
key: env.value,
147-
icon: this.getIcon(undefined, NODE_TRACKS, env.value, handleAdd, undefined),
158+
icon: this.getIcon(undefined, NODE_TRACKS, env.value, handleAdd, iconCreators),
148159
children: features
149160
.filter(feature => feature.properties?.env === env.value)
150161
.map((feature): TreeDataNode => ({
151162
title: this.nameFor(feature),
152163
key: this.idFor(feature),
153-
icon: this.getIcon(feature, this.idFor(feature), this.nameFor(feature), undefined, undefined),
164+
icon: this.getIcon(feature, this.idFor(feature), this.nameFor(feature), undefined, iconCreators),
154165
children: [],
155166
}))
156167
}))
157168

158-
root.children = root.children ? root.children.concat(...environments) : [...environments]
169+
// if time filter is applied, only include environments that contain features
170+
const validEnvironments = useTimeFilter ? environments.filter(env => !!env.children?.length) : environments
171+
172+
root.children = root.children ? root.children.concat(...validEnvironments) : [...validEnvironments]
159173
return root
160174
}
161175

@@ -166,36 +180,37 @@ export class TreeDataBuilder {
166180
* @param key The node key
167181
* @param dType The data type to filter by
168182
* @param handleAdd The add handler function
169-
* @param button Optional custom button
183+
* @param iconCreators The icon creator functions
184+
* @param useTimeFilter Whether to filter by time
170185
* @returns A TreeDataNode for the specified type
171186
*/
172187
static buildTypeNode(
173188
features: Feature[],
174189
title: string,
175190
key: string,
176-
dType: string,
191+
dType: FeatureTypes,
177192
handleAdd: HandleAddFunction,
178-
button?: React.ReactNode
179-
): TreeDataNode {
193+
iconCreators: IconCreators,
194+
useTimeFilter: boolean,
195+
iconOverride?: React.ReactNode
196+
): TreeDataNode | null {
180197
const children = features
181198
? this.findChildrenOfType(features, dType).map(child => {
182199
// Find the corresponding feature for this child
183200
const feature = features.find(f => this.idFor(f) === child.key)
184201
return {
185202
...child,
186-
icon: this.getIcon(feature, child.key, child.title, handleAdd, button),
203+
icon: this.getIcon(feature, child.key, child.title, handleAdd, iconCreators),
187204
}
188205
})
189206
: []
190207

208+
if (useTimeFilter && !children.length) return null
209+
191210
return {
192-
title: (
193-
<span>
194-
{title}
195-
</span>
196-
),
211+
title: iconCreators.createTitleElement(title),
197212
key,
198-
icon: this.getIcon(undefined, key, title, handleAdd, button), // Parent node gets plus icon
213+
icon: iconOverride || this.getIcon(undefined, key, title, handleAdd, iconCreators), // Parent node gets plus icon
199214
children,
200215
}
201216
}
@@ -204,15 +219,37 @@ export class TreeDataBuilder {
204219
* Build the complete tree data model
205220
* @param features The features to include
206221
* @param handleAdd The add handler function
222+
* @param iconCreators The icon creator functions
223+
* @param useTimeFilter Whether to filter features by time
224+
* @param timeStart The start time for filtering (if useTimeFilter is true)
225+
* @param timeEnd The end time for filtering (if useTimeFilter is true)
207226
* @returns An array of TreeDataNode objects representing the complete tree
208227
*/
209-
static buildTreeModel(features: Feature[], handleAdd: HandleAddFunction): TreeDataNode[] {
228+
static buildTreeModel(
229+
features: Feature[],
230+
handleAdd: HandleAddFunction,
231+
iconCreators: IconCreators,
232+
useTimeFilter: boolean = false,
233+
timeStart: number,
234+
timeEnd: number,
235+
zonesIcon: React.ReactNode
236+
): Array<TreeDataNode | null> {
237+
// If time filtering is enabled, filter the features
238+
let filteredFeatures = features
239+
if (useTimeFilter && timeStart !== 0 && timeEnd !== 0) {
240+
// Filter features based on time properties
241+
filteredFeatures = features.filter(feature =>
242+
// Include features that are visible in the current time period (or don't have time)
243+
featureIsVisibleInPeriod(feature, timeStart, timeEnd)
244+
)
245+
}
246+
210247
return [
211-
this.buildTrackNode(features, handleAdd),
212-
this.buildTypeNode(features, 'Buoy Fields', NODE_FIELDS, 'buoy-field', handleAdd),
213-
this.buildTypeNode(features, 'Zones', NODE_ZONES, 'zone', handleAdd),
214-
this.buildTypeNode(features, 'Reference Points', NODE_POINTS, 'reference-point', handleAdd),
215-
this.buildTypeNode(features, 'Backdrops', NODE_BACKDROPS, 'backdrop', handleAdd),
248+
this.buildTrackNode(filteredFeatures, handleAdd, iconCreators, useTimeFilter),
249+
this.buildTypeNode(filteredFeatures, 'Buoy Fields', NODE_FIELDS, BUOY_FIELD_TYPE, handleAdd, iconCreators, useTimeFilter, undefined),
250+
this.buildTypeNode(filteredFeatures, 'Zones', NODE_ZONES, ZONE_TYPE, handleAdd, iconCreators, useTimeFilter, zonesIcon),
251+
this.buildTypeNode(filteredFeatures, 'Reference Points', NODE_POINTS, REFERENCE_POINT_TYPE, handleAdd, iconCreators, useTimeFilter, undefined),
252+
this.buildTypeNode(filteredFeatures, 'Backdrops', NODE_BACKDROPS, BACKDROP_TYPE, handleAdd, iconCreators, useTimeFilter, undefined),
216253
]
217254
}
218255

0 commit comments

Comments
 (0)