@rural-environmental-registry/map_component 1.1.0 → 2.0.0

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.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/dist/handlers/measureToolsHandler.d.ts +1 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.mjs +16337 -14954
  6. package/dist/types/index.d.ts +118 -15
  7. package/dist/utils/geometryFileParseResult.d.ts +8 -0
  8. package/dist/utils/layersConfigNormalizer.d.ts +16 -0
  9. package/dist/utils/menuHistory.d.ts +3 -3
  10. package/dist/utils/normalizeGeoJsonToFeatures.d.ts +2 -0
  11. package/dist/utils/parseGeoJsonFile.d.ts +2 -0
  12. package/dist/utils/parseKmlFile.d.ts +2 -0
  13. package/dist/utils/parseShapefileZip.d.ts +2 -8
  14. package/dist/utils/{validateShapefileGeometry.d.ts → validateImportedPolygonGeometry.d.ts} +2 -2
  15. package/package.json +3 -2
  16. package/src/components/MapDPG.vue +90 -2
  17. package/src/components/coordinate/CoordinatePanel.vue +31 -8
  18. package/src/components/fa-icon/FontAwesomeIcon.vue +35 -5
  19. package/src/components/map/LeafletMap.vue +3 -1
  20. package/src/components/menu/ChildMenu.vue +307 -27
  21. package/src/components/menu/LayerMenu.vue +232 -37
  22. package/src/components/menu/ParentMenu.vue +261 -27
  23. package/src/components/menu/SectionMenu.vue +512 -0
  24. package/src/constants/coordinatePanel.ts +6 -5
  25. package/src/handlers/mapToolsHandler.ts +3 -1
  26. package/src/handlers/measureToolsHandler.ts +36 -27
  27. package/src/handlers/toolsConstants.ts +1 -0
  28. package/src/index.ts +47 -0
  29. package/src/types/index.ts +149 -14
  30. package/src/utils/geometryFileParseResult.ts +5 -0
  31. package/src/utils/layersConfigNormalizer.ts +273 -0
  32. package/src/utils/menuHistory.ts +35 -19
  33. package/src/utils/normalizeGeoJsonToFeatures.ts +52 -0
  34. package/src/utils/parseGeoJsonFile.ts +23 -0
  35. package/src/utils/parseKmlFile.ts +31 -0
  36. package/src/utils/parseShapefileZip.ts +4 -31
  37. package/src/utils/validateImportedPolygonGeometry.ts +62 -0
  38. package/src/utils/validateShapefileGeometry.ts +0 -54
package/src/index.ts CHANGED
@@ -3,4 +3,51 @@ import MapaDPG from './components/MapDPG.vue'
3
3
  export { createStableMarker, bindMarkerZoomStability, STABLE_MARKER_DEFAULTS } from './utils/stableMarker'
4
4
  export type { StableMarkerOptions } from './utils/stableMarker'
5
5
 
6
+ export type {
7
+ FaIconName,
8
+ SectionConfig,
9
+ SectionData,
10
+ SectionEditStatePayload,
11
+ SectionSelectPayload,
12
+ LayerActionConfig,
13
+ LayerActionStyle,
14
+ LayerActionDefaultItem,
15
+ LayerActionDefaults,
16
+ LayerActionPayload,
17
+ LayerMetricsConfig,
18
+ LayerMetrics,
19
+ LayerMetricsMap,
20
+ LayerInfoIconConfig,
21
+ LayerInfoIconsMap,
22
+ GroupActionConfig,
23
+ GroupActionsMap,
24
+ LayerData,
25
+ GroupLayerData,
26
+ LayersConfig,
27
+ LayersMenuConfig,
28
+ MapLayers,
29
+ MapOptionsConfig,
30
+ DrawingEvent,
31
+ MeasureCompleteEvent,
32
+ DescriptiveMemorial
33
+ } from './types'
34
+
35
+ export {
36
+ normalizeLayersConfig,
37
+ isSectionConfig,
38
+ isGroupConfig,
39
+ resolveLayerActions,
40
+ resolveEditControlAction,
41
+ shouldApplyMapOverlay,
42
+ isMenuSeparator,
43
+ collectLayerMetricsMap,
44
+ resolveLayerMetrics,
45
+ collectLayerInfoIconsMap,
46
+ resolveActiveLayerInfoIcons,
47
+ collectGroupActionsMap,
48
+ resolveActiveGroupActions,
49
+ withLayerInfoIconActive,
50
+ withGroupActionActive
51
+ } from './utils/layersConfigNormalizer'
52
+
6
53
  export default MapaDPG
@@ -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,156 @@ 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
+ style?: LayerActionStyle
51
+ disabled?: boolean
52
+ visible?: boolean
53
+ }
54
+
55
+ export type LayerActionDefaults = {
56
+ edit?: LayerActionDefaultItem
57
+ import?: LayerActionDefaultItem
58
+ delete?: LayerActionDefaultItem
59
+ cancel?: LayerActionDefaultItem
60
+ conclude?: LayerActionDefaultItem
61
+ }
62
+
63
+ export type LayerActionConfig = {
64
+ key: string
65
+ name: string
66
+ icon?: FaIconName
67
+ type?: 'edit' | 'import' | 'delete' | 'visibility' | 'custom' | 'cancel' | 'conclude'
68
+ disabled?: boolean
69
+ visible?: boolean
70
+ style?: LayerActionStyle
71
+ }
72
+
73
+ export type LayerMetricsConfig = {
74
+ title: string
75
+ type: string
76
+ value: string | number
77
+ style?: LayerActionStyle
78
+ }
79
+
80
+ /** @deprecated Use LayerMetricsConfig */
81
+ export type LayerMetrics = LayerMetricsConfig
82
+
83
+ export type LayerMetricsMap = Record<string, LayerMetricsConfig[]>
84
+
85
+ export type LayerInfoIconConfig = {
86
+
87
+ key?: string
88
+ icon: FaIconName
89
+ active: boolean
90
+ tooltip?: string
91
+ style?: Partial<Pick<LayerActionStyle, 'color' | 'backgroundColor'>>
92
+ }
93
+
94
+ export type LayerInfoIconsMap = Record<string, LayerInfoIconConfig[]>
95
+
96
+ export type GroupActionConfig = LayerActionConfig & {
36
97
  active: boolean
98
+ }
99
+
100
+
101
+ export type GroupActionsMap = Record<string, GroupActionConfig[]>
102
+
103
+ export type SectionConfig = {
104
+ name?: string
105
+ key: string
106
+ groups: GroupLayerData[]
107
+ actionDefaults?: LayerActionDefaults
108
+ }
109
+
110
+ /** @deprecated Use SectionConfig */
111
+ export type SectionData = SectionConfig
112
+
113
+ export type LayerData = {
37
114
  key: string
38
- toggle: {
115
+ name: string
116
+ active: boolean
117
+ activeDefault: boolean
118
+ role?: 'layer' | 'separator'
119
+ tooltip?: string
120
+ toggle?: {
39
121
  active: string
40
122
  inactive: string
41
123
  }
42
- style: {
124
+ visibility?: {
125
+ labelShow?: string
126
+ labelHide?: string
127
+ show?: boolean
128
+ }
129
+ style?: {
43
130
  color: string
44
131
  fillColor: string
132
+ icon?: string
45
133
  }
46
- options?: any
134
+ baseUrl?: string
135
+ layers?: string
136
+ format?: string
137
+ transparent?: boolean
138
+ geojson?: GeoJsonObject | GeoJsonObject[]
139
+ cqlFilter?: string
140
+ options?: Record<string, unknown>
141
+
142
+ mapSource?: 'wms' | 'geojson' | 'none' | 'consumer'
143
+ actions?: LayerActionConfig[]
144
+ metrics?: LayerMetricsConfig[]
145
+ infoIcons?: LayerInfoIconConfig[]
146
+ required?: boolean
147
+ editActions?: LayerActionConfig[]
148
+ meta?: Record<string, unknown>
47
149
  }
48
150
 
49
151
  export type GroupLayerData = {
50
152
  name: string
51
153
  key: string
52
- toggle: {
154
+ toggle?: {
53
155
  active: string
54
156
  inactive: string
55
157
  }
158
+ visibility?: {
159
+ show?: boolean
160
+ labelShow?: string
161
+ labelHide?: string
162
+ }
163
+ collapsed?: boolean
164
+ actions?: GroupActionConfig[]
56
165
  layers: LayerData[]
166
+ meta?: Record<string, unknown>
57
167
  }
58
168
 
59
- export type LayersConfig = GroupLayerData[]
169
+ export type LayersConfig = GroupLayerData[] | SectionConfig[]
170
+
171
+ export type LayerActionPayload = {
172
+ actionKey: string
173
+ actionType?: LayerActionConfig['type']
174
+ sectionKey?: string
175
+ groupKey?: string
176
+ layerKey?: string
177
+ layer?: LayerData
178
+ source: 'child-menu' | 'edit-panel' | 'group-menu'
179
+ }
180
+
181
+ export type SectionEditStatePayload = {
182
+ sectionKey: string | null
183
+ layerKey: string | null
184
+ layer: LayerData | null
185
+ }
186
+
187
+ export type SectionSelectPayload = {
188
+ sectionKey: string
189
+ }
60
190
 
61
191
  export type DrawingEvent = {
62
192
  type: 'created' | 'edited' | 'deleted'
@@ -87,7 +217,6 @@ export type MapConfigConfig = MapOptions & {
87
217
  id: string
88
218
  removeControlLayers?: boolean
89
219
  zoomControlPosition?: ControlPosition
90
- /** Reposiciona marcadores após zoom (evita drift de divIcon). Padrão: true. */
91
220
  stabilizeMarkersOnZoom?: boolean
92
221
  }
93
222
 
@@ -99,6 +228,10 @@ export type LayersMenuConfig = {
99
228
  size: 'small' | 'medium' | 'large'
100
229
  removeMenu?: boolean
101
230
  persist: boolean
231
+ editingLayerKey?: string | null
232
+ selectedSectionKey?: string | null
233
+ visibilityMode?: 'switch' | 'eye'
234
+ defaultOpen?: boolean
102
235
  }
103
236
 
104
237
  export type MemorialConfig = {
@@ -131,6 +264,7 @@ export type MapToolsConfig = {
131
264
  show?: boolean
132
265
  title?: string
133
266
  }
267
+ showInteractionPanel?: boolean
134
268
  texts?: {
135
269
  measureResult?: string
136
270
  measureLength?: string
@@ -240,6 +374,7 @@ export type CoordinatePanelTexts = {
240
374
  dragShapefileZip?: string
241
375
  shapefileZipInfo?: string
242
376
  shapefileAppliedSuccess?: string
377
+ geometryImportUnsupportedFormat?: string
243
378
  placeholderLongitude?: string
244
379
  placeholderLatitude?: string
245
380
  placeholderAzimuth?: string
@@ -0,0 +1,5 @@
1
+ import type { Feature } from 'geojson'
2
+
3
+ export type GeometryFileParseResult =
4
+ | { ok: true; features: Feature[] }
5
+ | { ok: false; error: string }
@@ -0,0 +1,273 @@
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
+ style: defaults.edit.style,
114
+ disabled: defaults.edit.disabled
115
+ })
116
+ }
117
+
118
+ if (defaults.import) {
119
+ actions.push({
120
+ key: defaults.import.key ?? 'import',
121
+ name: defaults.import.name,
122
+ icon: defaults.import.icon ?? 'upload',
123
+ type: 'import',
124
+ style: defaults.import.style,
125
+ disabled: defaults.import.disabled
126
+ })
127
+ }
128
+
129
+ if (defaults.delete) {
130
+ actions.push({
131
+ key: defaults.delete.key ?? 'delete',
132
+ name: defaults.delete.name,
133
+ icon: defaults.delete.icon ?? 'trash',
134
+ type: 'delete',
135
+ style: defaults.delete.style,
136
+ disabled: defaults.delete.disabled
137
+ })
138
+ }
139
+
140
+ return actions.filter((action) => action.visible !== false)
141
+ }
142
+
143
+
144
+ export const resolveEditControlAction = (
145
+ defaults: LayerActionDefaults | undefined,
146
+ kind: 'cancel' | 'conclude'
147
+ ): LayerActionConfig => {
148
+ const configured = defaults?.[kind]
149
+ const fallbackName = kind === 'cancel' ? 'Cancelar' : 'Concluir'
150
+
151
+ return {
152
+ key: configured?.key ?? kind,
153
+ name: configured?.name ?? fallbackName,
154
+ icon: configured?.icon,
155
+ type: kind,
156
+ style: configured?.style,
157
+ disabled: configured?.disabled,
158
+ visible: configured?.visible
159
+ }
160
+ }
161
+
162
+ export const isMenuSeparator = (layer: LayerData | null | undefined): boolean => {
163
+ return layer?.role === 'separator' || !!layer?.meta?.isSeparator
164
+ }
165
+
166
+ export const shouldApplyMapOverlay = (layer: LayerData): boolean => {
167
+ if (isMenuSeparator(layer)) return false
168
+ if (layer.mapSource === 'none' || layer.mapSource === 'consumer') return false
169
+ if (layer.mapSource === 'wms' || layer.mapSource === 'geojson') return true
170
+ if (layer.geojson) return true
171
+ if (layer.baseUrl) return true
172
+ return false
173
+ }
174
+
175
+ export const collectLayerMetricsMap = (config?: LayersConfig | null): LayerMetricsMap => {
176
+ const result: LayerMetricsMap = {}
177
+
178
+ normalizeLayersConfig(config).forEach((section) => {
179
+ section.groups.forEach((group) => {
180
+ group.layers?.forEach((layer: LayerData) => {
181
+ if (layer.metrics?.length) {
182
+ result[layer.key] = layer.metrics
183
+ }
184
+ })
185
+ })
186
+ })
187
+
188
+ return result
189
+ }
190
+
191
+ export const resolveLayerMetrics = (layer: LayerData | null | undefined): LayerMetricsConfig[] => {
192
+ return layer?.metrics?.length ? layer.metrics : []
193
+ }
194
+
195
+ export const collectLayerInfoIconsMap = (config?: LayersConfig | null): LayerInfoIconsMap => {
196
+ const result: LayerInfoIconsMap = {}
197
+
198
+ normalizeLayersConfig(config).forEach((section) => {
199
+ section.groups.forEach((group) => {
200
+ group.layers?.forEach((layer: LayerData) => {
201
+ if (layer.infoIcons?.length) {
202
+ result[layer.key] = layer.infoIcons
203
+ }
204
+ })
205
+ })
206
+ })
207
+
208
+ return result
209
+ }
210
+
211
+ export const resolveActiveLayerInfoIcons = (
212
+ layer: LayerData | null | undefined
213
+ ): LayerInfoIconConfig[] => {
214
+ return (layer?.infoIcons ?? []).filter((item) => item.active)
215
+ }
216
+
217
+ export const collectGroupActionsMap = (config?: LayersConfig | null): GroupActionsMap => {
218
+ const result: GroupActionsMap = {}
219
+
220
+ normalizeLayersConfig(config).forEach((section) => {
221
+ section.groups.forEach((group) => {
222
+ if (group.actions?.length) {
223
+ result[group.key] = group.actions
224
+ }
225
+ })
226
+ })
227
+
228
+ return result
229
+ }
230
+
231
+ export const resolveActiveGroupActions = (
232
+ group: GroupLayerData | null | undefined
233
+ ): GroupActionConfig[] => {
234
+ return (group?.actions ?? []).filter(
235
+ (action) => action.active && action.visible !== false
236
+ )
237
+ }
238
+
239
+ export const withLayerInfoIconActive = (
240
+ infoIcons: LayerInfoIconConfig[] | undefined,
241
+ iconRef: string,
242
+ active: boolean
243
+ ): LayerInfoIconConfig[] | null => {
244
+ if (!infoIcons?.length) return null
245
+
246
+ let targetIndex = infoIcons.findIndex((item) => item.key === iconRef)
247
+ if (targetIndex < 0) {
248
+ const parsed = Number.parseInt(iconRef, 10)
249
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed >= infoIcons.length) {
250
+ return null
251
+ }
252
+ targetIndex = parsed
253
+ }
254
+
255
+ return infoIcons.map((item, idx) =>
256
+ idx === targetIndex ? { ...item, active } : item
257
+ )
258
+ }
259
+
260
+ export const withGroupActionActive = (
261
+ actions: GroupActionConfig[] | undefined,
262
+ actionKey: string,
263
+ active: boolean
264
+ ): GroupActionConfig[] | null => {
265
+ if (!actions?.length) return null
266
+
267
+ const exists = actions.some((action) => action.key === actionKey)
268
+ if (!exists) return null
269
+
270
+ return actions.map((action) =>
271
+ action.key === actionKey ? { ...action, active } : action
272
+ )
273
+ }
@@ -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
 
@@ -0,0 +1,52 @@
1
+ import type { Feature, FeatureCollection, Geometry } from 'geojson'
2
+
3
+ function isFeatureCollection(value: unknown): value is FeatureCollection {
4
+ return (
5
+ typeof value === 'object' &&
6
+ value !== null &&
7
+ 'type' in value &&
8
+ (value as FeatureCollection).type === 'FeatureCollection' &&
9
+ 'features' in value &&
10
+ Array.isArray(value.features)
11
+ )
12
+ }
13
+
14
+ function isFeature(value: unknown): value is Feature {
15
+ return (
16
+ typeof value === 'object' &&
17
+ value !== null &&
18
+ 'type' in value &&
19
+ (value as Feature).type === 'Feature' &&
20
+ 'geometry' in value
21
+ )
22
+ }
23
+
24
+ function isGeometry(value: unknown): value is Geometry {
25
+ return (
26
+ typeof value === 'object' &&
27
+ value !== null &&
28
+ 'type' in value &&
29
+ !('features' in value) &&
30
+ (value as { type: string }).type !== 'Feature'
31
+ )
32
+ }
33
+
34
+ export function normalizeGeoJsonToFeatures(geojson: unknown): Feature[] {
35
+ if (Array.isArray(geojson)) {
36
+ return geojson.flatMap(item => normalizeGeoJsonToFeatures(item))
37
+ }
38
+
39
+ if (isFeatureCollection(geojson)) {
40
+ return geojson.features
41
+ }
42
+
43
+ if (isFeature(geojson)) {
44
+ return [geojson]
45
+ }
46
+
47
+ if (isGeometry(geojson)) {
48
+ return [{ type: 'Feature', properties: null, geometry: geojson }]
49
+ }
50
+
51
+ return []
52
+ }