@rural-environmental-registry/map_component 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,19 @@ export type FaIconName =
15
15
  | 'pencil'
16
16
  | 'trash'
17
17
  | 'chevron-down'
18
+ | 'eye'
19
+ | 'eye-slash'
20
+ | 'circle-info'
21
+ | 'circle-exclamation'
22
+ | 'triangle-exclamation'
23
+ | 'circle-check'
24
+ | 'circle-xmark'
25
+ | 'ban'
26
+ | 'lock'
27
+ | 'unlock'
28
+ | 'thumbtack'
29
+ | 'thumb-tack'
30
+ | 'map-pin'
18
31
 
19
32
  export type DrawnArea = {
20
33
  m2: number
@@ -24,39 +37,158 @@ export type DrawnArea = {
24
37
 
25
38
  export type IncrementedLayer = Layer & { drawnArea: DrawnArea }
26
39
 
27
- export type LayerData = {
28
- baseUrl: string
29
- geojson?: GeoJsonObject | GeoJsonObject[]
30
- layers: string
31
- format: string
32
- transparent: boolean
40
+ export type LayerActionStyle = {
41
+ backgroundColor: string
42
+ color: string
43
+ borderColor?: string
44
+ }
45
+
46
+ export type LayerActionDefaultItem = {
47
+ key?: string
33
48
  name: string
34
- activeDefault: boolean
35
- cqlFilter?: string
49
+ icon?: FaIconName
50
+ tooltip?: string
51
+ style?: LayerActionStyle
52
+ disabled?: boolean
53
+ visible?: boolean
54
+ }
55
+
56
+ export type LayerActionDefaults = {
57
+ edit?: LayerActionDefaultItem
58
+ import?: LayerActionDefaultItem
59
+ delete?: LayerActionDefaultItem
60
+ cancel?: LayerActionDefaultItem
61
+ conclude?: LayerActionDefaultItem
62
+ }
63
+
64
+ export type LayerActionConfig = {
65
+ key: string
66
+ name: string
67
+ icon?: FaIconName
68
+ type?: 'edit' | 'import' | 'delete' | 'visibility' | 'custom' | 'cancel' | 'conclude'
69
+ disabled?: boolean
70
+ visible?: boolean
71
+ tooltip?: string
72
+ style?: LayerActionStyle
73
+ }
74
+
75
+ export type LayerMetricsConfig = {
76
+ title: string
77
+ type: string
78
+ value: string | number
79
+ style?: LayerActionStyle
80
+ }
81
+
82
+ /** @deprecated Use LayerMetricsConfig */
83
+ export type LayerMetrics = LayerMetricsConfig
84
+
85
+ export type LayerMetricsMap = Record<string, LayerMetricsConfig[]>
86
+
87
+ export type LayerInfoIconConfig = {
88
+
89
+ key?: string
90
+ icon: FaIconName
91
+ active: boolean
92
+ tooltip?: string
93
+ style?: Partial<Pick<LayerActionStyle, 'color' | 'backgroundColor'>>
94
+ }
95
+
96
+ export type LayerInfoIconsMap = Record<string, LayerInfoIconConfig[]>
97
+
98
+ export type GroupActionConfig = LayerActionConfig & {
36
99
  active: boolean
100
+ }
101
+
102
+
103
+ export type GroupActionsMap = Record<string, GroupActionConfig[]>
104
+
105
+ export type SectionConfig = {
106
+ name?: string
107
+ key: string
108
+ groups: GroupLayerData[]
109
+ actionDefaults?: LayerActionDefaults
110
+ }
111
+
112
+ /** @deprecated Use SectionConfig */
113
+ export type SectionData = SectionConfig
114
+
115
+ export type LayerData = {
37
116
  key: string
38
- toggle: {
117
+ name: string
118
+ active: boolean
119
+ activeDefault: boolean
120
+ role?: 'layer' | 'separator'
121
+ tooltip?: string
122
+ toggle?: {
39
123
  active: string
40
124
  inactive: string
41
125
  }
42
- style: {
126
+ visibility?: {
127
+ labelShow?: string
128
+ labelHide?: string
129
+ show?: boolean
130
+ }
131
+ style?: {
43
132
  color: string
44
133
  fillColor: string
134
+ icon?: string
45
135
  }
46
- options?: any
136
+ baseUrl?: string
137
+ layers?: string
138
+ format?: string
139
+ transparent?: boolean
140
+ geojson?: GeoJsonObject | GeoJsonObject[]
141
+ cqlFilter?: string
142
+ options?: Record<string, unknown>
143
+
144
+ mapSource?: 'wms' | 'geojson' | 'none' | 'consumer'
145
+ actions?: LayerActionConfig[]
146
+ metrics?: LayerMetricsConfig[]
147
+ infoIcons?: LayerInfoIconConfig[]
148
+ required?: boolean
149
+ editActions?: LayerActionConfig[]
150
+ meta?: Record<string, unknown>
47
151
  }
48
152
 
49
153
  export type GroupLayerData = {
50
154
  name: string
51
155
  key: string
52
- toggle: {
156
+ toggle?: {
53
157
  active: string
54
158
  inactive: string
55
159
  }
160
+ visibility?: {
161
+ show?: boolean
162
+ labelShow?: string
163
+ labelHide?: string
164
+ }
165
+ collapsed?: boolean
166
+ actions?: GroupActionConfig[]
56
167
  layers: LayerData[]
168
+ meta?: Record<string, unknown>
57
169
  }
58
170
 
59
- export type LayersConfig = GroupLayerData[]
171
+ export type LayersConfig = GroupLayerData[] | SectionConfig[]
172
+
173
+ export type LayerActionPayload = {
174
+ actionKey: string
175
+ actionType?: LayerActionConfig['type']
176
+ sectionKey?: string
177
+ groupKey?: string
178
+ layerKey?: string
179
+ layer?: LayerData
180
+ source: 'child-menu' | 'edit-panel' | 'group-menu'
181
+ }
182
+
183
+ export type SectionEditStatePayload = {
184
+ sectionKey: string | null
185
+ layerKey: string | null
186
+ layer: LayerData | null
187
+ }
188
+
189
+ export type SectionSelectPayload = {
190
+ sectionKey: string
191
+ }
60
192
 
61
193
  export type DrawingEvent = {
62
194
  type: 'created' | 'edited' | 'deleted'
@@ -87,7 +219,6 @@ export type MapConfigConfig = MapOptions & {
87
219
  id: string
88
220
  removeControlLayers?: boolean
89
221
  zoomControlPosition?: ControlPosition
90
- /** Reposiciona marcadores após zoom (evita drift de divIcon). Padrão: true. */
91
222
  stabilizeMarkersOnZoom?: boolean
92
223
  }
93
224
 
@@ -99,6 +230,10 @@ export type LayersMenuConfig = {
99
230
  size: 'small' | 'medium' | 'large'
100
231
  removeMenu?: boolean
101
232
  persist: boolean
233
+ editingLayerKey?: string | null
234
+ selectedSectionKey?: string | null
235
+ visibilityMode?: 'switch' | 'eye'
236
+ defaultOpen?: boolean
102
237
  }
103
238
 
104
239
  export type MemorialConfig = {
@@ -131,6 +266,7 @@ export type MapToolsConfig = {
131
266
  show?: boolean
132
267
  title?: string
133
268
  }
269
+ showInteractionPanel?: boolean
134
270
  texts?: {
135
271
  measureResult?: string
136
272
  measureLength?: string
@@ -0,0 +1,276 @@
1
+ import {
2
+ GroupActionConfig,
3
+ GroupActionsMap,
4
+ GroupLayerData,
5
+ LayerActionConfig,
6
+ LayerActionDefaults,
7
+ LayerData,
8
+ LayerInfoIconConfig,
9
+ LayerInfoIconsMap,
10
+ LayerMetricsConfig,
11
+ LayerMetricsMap,
12
+ LayersConfig,
13
+ SectionConfig
14
+ } from '../types'
15
+
16
+ type GroupLike = GroupLayerData & {
17
+ customLayers?: LayerData[]
18
+ }
19
+
20
+ export const isSectionConfig = (item: unknown): item is SectionConfig => {
21
+ return (
22
+ typeof item === 'object' &&
23
+ item !== null &&
24
+ Array.isArray((item as SectionConfig).groups)
25
+ )
26
+ }
27
+
28
+ export const isGroupConfig = (item: unknown): item is GroupLayerData => {
29
+ if (typeof item !== 'object' || item === null) return false
30
+ if (Array.isArray((item as SectionConfig).groups)) return false
31
+
32
+ const group = item as GroupLike
33
+ return Array.isArray(group.layers) || Array.isArray(group.customLayers)
34
+ }
35
+
36
+ const normalizeGroup = (group: GroupLike): GroupLayerData => {
37
+ const layers = group.layers ?? group.customLayers ?? []
38
+ const { customLayers: _alias, ...rest } = group
39
+ return {
40
+ ...rest,
41
+ layers
42
+ }
43
+ }
44
+
45
+ export const normalizeLayersConfig = (config?: LayersConfig | null): SectionConfig[] => {
46
+ if (!config?.length) return []
47
+
48
+ const allSections = config.every(isSectionConfig)
49
+ const allGroups = config.every(isGroupConfig)
50
+
51
+ if (allSections) {
52
+ return (config as SectionConfig[]).map((section) => ({
53
+ ...section,
54
+ groups: section.groups.map((group) => normalizeGroup(group as GroupLike))
55
+ }))
56
+ }
57
+
58
+ if (allGroups) {
59
+ return [
60
+ {
61
+ key: 'default',
62
+ groups: (config as GroupLike[]).map(normalizeGroup)
63
+ }
64
+ ]
65
+ }
66
+
67
+ console.warn(
68
+ '[map_component] LayersConfig misto (sections e groups). Usando apenas itens reconhecidos como section ou group.'
69
+ )
70
+
71
+ const sections: SectionConfig[] = []
72
+ const orphanGroups: GroupLayerData[] = []
73
+ const mixedItems = config as Array<SectionConfig | GroupLayerData>
74
+
75
+ for (const item of mixedItems) {
76
+ if (isSectionConfig(item)) {
77
+ sections.push({
78
+ ...item,
79
+ groups: item.groups.map((group) => normalizeGroup(group as GroupLike))
80
+ })
81
+ } else if (isGroupConfig(item)) {
82
+ orphanGroups.push(normalizeGroup(item as GroupLike))
83
+ }
84
+ }
85
+
86
+ if (orphanGroups.length) {
87
+ sections.push({ key: 'default', groups: orphanGroups })
88
+ }
89
+
90
+ return sections
91
+ }
92
+
93
+ export const resolveLayerActions = (
94
+ layer: LayerData,
95
+ defaults?: LayerActionDefaults
96
+ ): LayerActionConfig[] => {
97
+
98
+ if (Array.isArray(layer.actions)) {
99
+ if (layer.actions.length === 0) return []
100
+ return layer.actions.filter((action) => action.visible !== false)
101
+ }
102
+
103
+ if (!defaults) return []
104
+
105
+ const actions: LayerActionConfig[] = []
106
+
107
+ if (defaults.edit) {
108
+ actions.push({
109
+ key: defaults.edit.key ?? 'edit',
110
+ name: defaults.edit.name,
111
+ icon: defaults.edit.icon ?? 'pencil',
112
+ type: 'edit',
113
+ tooltip: defaults.edit.tooltip,
114
+ style: defaults.edit.style,
115
+ disabled: defaults.edit.disabled
116
+ })
117
+ }
118
+
119
+ if (defaults.import) {
120
+ actions.push({
121
+ key: defaults.import.key ?? 'import',
122
+ name: defaults.import.name,
123
+ icon: defaults.import.icon ?? 'upload',
124
+ type: 'import',
125
+ tooltip: defaults.import.tooltip,
126
+ style: defaults.import.style,
127
+ disabled: defaults.import.disabled
128
+ })
129
+ }
130
+
131
+ if (defaults.delete) {
132
+ actions.push({
133
+ key: defaults.delete.key ?? 'delete',
134
+ name: defaults.delete.name,
135
+ icon: defaults.delete.icon ?? 'trash',
136
+ type: 'delete',
137
+ tooltip: defaults.delete.tooltip,
138
+ style: defaults.delete.style,
139
+ disabled: defaults.delete.disabled
140
+ })
141
+ }
142
+
143
+ return actions.filter((action) => action.visible !== false)
144
+ }
145
+
146
+
147
+ export const resolveEditControlAction = (
148
+ defaults: LayerActionDefaults | undefined,
149
+ kind: 'cancel' | 'conclude'
150
+ ): LayerActionConfig => {
151
+ const configured = defaults?.[kind]
152
+ const fallbackName = kind === 'cancel' ? 'Cancelar' : 'Concluir'
153
+
154
+ return {
155
+ key: configured?.key ?? kind,
156
+ name: configured?.name ?? fallbackName,
157
+ icon: configured?.icon,
158
+ type: kind,
159
+ style: configured?.style,
160
+ disabled: configured?.disabled,
161
+ visible: configured?.visible
162
+ }
163
+ }
164
+
165
+ export const isMenuSeparator = (layer: LayerData | null | undefined): boolean => {
166
+ return layer?.role === 'separator' || !!layer?.meta?.isSeparator
167
+ }
168
+
169
+ export const shouldApplyMapOverlay = (layer: LayerData): boolean => {
170
+ if (isMenuSeparator(layer)) return false
171
+ if (layer.mapSource === 'none' || layer.mapSource === 'consumer') return false
172
+ if (layer.mapSource === 'wms' || layer.mapSource === 'geojson') return true
173
+ if (layer.geojson) return true
174
+ if (layer.baseUrl) return true
175
+ return false
176
+ }
177
+
178
+ export const collectLayerMetricsMap = (config?: LayersConfig | null): LayerMetricsMap => {
179
+ const result: LayerMetricsMap = {}
180
+
181
+ normalizeLayersConfig(config).forEach((section) => {
182
+ section.groups.forEach((group) => {
183
+ group.layers?.forEach((layer: LayerData) => {
184
+ if (layer.metrics?.length) {
185
+ result[layer.key] = layer.metrics
186
+ }
187
+ })
188
+ })
189
+ })
190
+
191
+ return result
192
+ }
193
+
194
+ export const resolveLayerMetrics = (layer: LayerData | null | undefined): LayerMetricsConfig[] => {
195
+ return layer?.metrics?.length ? layer.metrics : []
196
+ }
197
+
198
+ export const collectLayerInfoIconsMap = (config?: LayersConfig | null): LayerInfoIconsMap => {
199
+ const result: LayerInfoIconsMap = {}
200
+
201
+ normalizeLayersConfig(config).forEach((section) => {
202
+ section.groups.forEach((group) => {
203
+ group.layers?.forEach((layer: LayerData) => {
204
+ if (layer.infoIcons?.length) {
205
+ result[layer.key] = layer.infoIcons
206
+ }
207
+ })
208
+ })
209
+ })
210
+
211
+ return result
212
+ }
213
+
214
+ export const resolveActiveLayerInfoIcons = (
215
+ layer: LayerData | null | undefined
216
+ ): LayerInfoIconConfig[] => {
217
+ return (layer?.infoIcons ?? []).filter((item) => item.active)
218
+ }
219
+
220
+ export const collectGroupActionsMap = (config?: LayersConfig | null): GroupActionsMap => {
221
+ const result: GroupActionsMap = {}
222
+
223
+ normalizeLayersConfig(config).forEach((section) => {
224
+ section.groups.forEach((group) => {
225
+ if (group.actions?.length) {
226
+ result[group.key] = group.actions
227
+ }
228
+ })
229
+ })
230
+
231
+ return result
232
+ }
233
+
234
+ export const resolveActiveGroupActions = (
235
+ group: GroupLayerData | null | undefined
236
+ ): GroupActionConfig[] => {
237
+ return (group?.actions ?? []).filter(
238
+ (action) => action.active && action.visible !== false
239
+ )
240
+ }
241
+
242
+ export const withLayerInfoIconActive = (
243
+ infoIcons: LayerInfoIconConfig[] | undefined,
244
+ iconRef: string,
245
+ active: boolean
246
+ ): LayerInfoIconConfig[] | null => {
247
+ if (!infoIcons?.length) return null
248
+
249
+ let targetIndex = infoIcons.findIndex((item) => item.key === iconRef)
250
+ if (targetIndex < 0) {
251
+ const parsed = Number.parseInt(iconRef, 10)
252
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed >= infoIcons.length) {
253
+ return null
254
+ }
255
+ targetIndex = parsed
256
+ }
257
+
258
+ return infoIcons.map((item, idx) =>
259
+ idx === targetIndex ? { ...item, active } : item
260
+ )
261
+ }
262
+
263
+ export const withGroupActionActive = (
264
+ actions: GroupActionConfig[] | undefined,
265
+ actionKey: string,
266
+ active: boolean
267
+ ): GroupActionConfig[] | null => {
268
+ if (!actions?.length) return null
269
+
270
+ const exists = actions.some((action) => action.key === actionKey)
271
+ if (!exists) return null
272
+
273
+ return actions.map((action) =>
274
+ action.key === actionKey ? { ...action, active } : action
275
+ )
276
+ }
@@ -1,10 +1,11 @@
1
1
  import { LayerData } from '../types'
2
2
 
3
3
  const MENU_HISTORY_KEY = 'menuHistory'
4
+ const MENU_COLLAPSE_KEY = 'menuCollapseHistory'
4
5
 
5
- const readHistory = (): Record<string, boolean> => {
6
+ const readJsonObject = (storageKey: string): Record<string, boolean> => {
6
7
  try {
7
- const raw = window.sessionStorage.getItem(MENU_HISTORY_KEY)
8
+ const raw = window.sessionStorage.getItem(storageKey)
8
9
  if (!raw) return {}
9
10
 
10
11
  const parsed: unknown = JSON.parse(raw)
@@ -18,31 +19,35 @@ const readHistory = (): Record<string, boolean> => {
18
19
  }
19
20
  }
20
21
 
21
- export const setHistory = (layer: LayerData): void => {
22
+ const writeJsonObject = (storageKey: string, value: Record<string, boolean>): void => {
22
23
  try {
23
- const history = readHistory()
24
- window.sessionStorage.setItem(
25
- MENU_HISTORY_KEY,
26
- JSON.stringify({ ...history, [layer.key]: layer.active })
27
- )
24
+ window.sessionStorage.setItem(storageKey, JSON.stringify(value))
28
25
  } catch {
29
- // sessionStorage indisponível ou JSON inválido — ignora persistência
30
26
  }
31
27
  }
32
28
 
33
- /** Retorna o estado salvo da camada ou `undefined` se não houver histórico. */
29
+ export const setHistory = (layer: LayerData): void => {
30
+ const history = readJsonObject(MENU_HISTORY_KEY)
31
+ writeJsonObject(MENU_HISTORY_KEY, { ...history, [layer.key]: layer.active })
32
+ }
33
+
34
34
  export const getHistory = (layerKey: string): boolean | undefined => {
35
- try {
36
- const history = readHistory()
37
- const isActive = history[layerKey]
35
+ const history = readJsonObject(MENU_HISTORY_KEY)
36
+ const isActive = history[layerKey]
37
+ return isActive !== undefined ? isActive : undefined
38
+ }
38
39
 
39
- return isActive !== undefined ? isActive : undefined
40
- } catch {
41
- return undefined
42
- }
40
+ export const setGroupCollapsedHistory = (groupKey: string, collapsed: boolean): void => {
41
+ const history = readJsonObject(MENU_COLLAPSE_KEY)
42
+ writeJsonObject(MENU_COLLAPSE_KEY, { ...history, [groupKey]: collapsed })
43
+ }
44
+
45
+ export const getGroupCollapsedHistory = (groupKey: string): boolean | undefined => {
46
+ const history = readJsonObject(MENU_COLLAPSE_KEY)
47
+ const collapsed = history[groupKey]
48
+ return collapsed !== undefined ? collapsed : undefined
43
49
  }
44
50
 
45
- /** Resolve `active`/`activeDefault` sem mutar o objeto original (mesma regra do fluxo legado). */
46
51
  export const resolveLayerActiveState = (layer: LayerData, persist: boolean): LayerData => {
47
52
  const savedActive = persist ? getHistory(layer.key) : undefined
48
53
 
@@ -61,7 +66,18 @@ export const resolveLayerActiveState = (layer: LayerData, persist: boolean): Lay
61
66
  return { ...layer, active, activeDefault }
62
67
  }
63
68
 
64
- /** Indica se a camada deve ser aplicada ao mapa na inicialização. */
69
+ export const resolveGroupCollapsedState = (
70
+ groupKey: string,
71
+ collapsedFromConfig: boolean | undefined,
72
+ persist: boolean
73
+ ): boolean => {
74
+ if (persist) {
75
+ const saved = getGroupCollapsedHistory(groupKey)
76
+ if (saved !== undefined) return saved
77
+ }
78
+ return collapsedFromConfig ?? false
79
+ }
80
+
65
81
  export const shouldInitLayerOnMap = (layer: LayerData, persist: boolean): boolean => {
66
82
  const resolved = resolveLayerActiveState(layer, persist)
67
83