@rural-environmental-registry/map_component 1.0.0 → 1.2.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.
- package/README.md +444 -0
- package/dist/constants/coordinatePanel.d.ts +1 -1
- package/dist/handlers/drawingControl.d.ts +0 -1
- package/dist/handlers/mapHandler.d.ts +8 -3
- package/dist/handlers/mapToolsHandler.d.ts +39 -0
- package/dist/handlers/measureToolsHandler.d.ts +51 -0
- package/dist/handlers/memorialButtonControl.d.ts +19 -0
- package/dist/handlers/toolsConstants.d.ts +3 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +32914 -25131
- package/dist/types/index.d.ts +69 -0
- package/dist/utils/areaCalculator.d.ts +3 -0
- package/dist/utils/drawingPathOptions.d.ts +8 -0
- package/dist/utils/geometryCalculator.d.ts +13 -0
- package/dist/utils/geometryFileParseResult.d.ts +8 -0
- package/dist/utils/memorialLayer.d.ts +3 -0
- package/dist/utils/menuHistory.d.ts +8 -0
- package/dist/utils/normalizeGeoJsonToFeatures.d.ts +2 -0
- package/dist/utils/parseGeoJsonFile.d.ts +2 -0
- package/dist/utils/parseKmlFile.d.ts +2 -0
- package/dist/utils/parseShapefileZip.d.ts +3 -0
- package/dist/utils/stableMarker.d.ts +15 -0
- package/dist/utils/validateImportedPolygonGeometry.d.ts +9 -0
- package/package.json +22 -4
- package/src/assets/icons/file-import-regular-full-gray.svg +1 -0
- package/src/assets/icons/file-lines-regular-full-gray.svg +1 -0
- package/src/assets/icons/layer-icon.svg +3 -0
- package/src/assets/icons/loading.gif +0 -0
- package/src/assets/icons/marker-icon.svg +1 -0
- package/src/assets/icons/measure-icon.svg +5 -0
- package/src/assets/layers/mapLayers.json +16 -0
- package/src/assets/main.css +187 -0
- package/src/assets/styles/element-plus.css +25 -0
- package/src/assets/styles/layers-menu.css +95 -0
- package/src/assets/styles/leaflet.css +361 -0
- package/src/components/MapDPG.vue +302 -0
- package/src/components/coordinate/CoordinatePanel.vue +1206 -0
- package/src/components/fa-icon/FontAwesomeIcon.vue +44 -0
- package/src/components/loading/Loading.vue +25 -0
- package/src/components/map/LeafletMap.vue +183 -0
- package/src/components/menu/ChildMenu.vue +112 -0
- package/src/components/menu/LayerMenu.vue +294 -0
- package/src/components/menu/ParentMenu.vue +132 -0
- package/src/constants/coordinatePanel.ts +68 -0
- package/src/handlers/constants.ts +35 -0
- package/src/handlers/drawingControl.ts +143 -0
- package/src/handlers/mapHandler.ts +124 -0
- package/src/handlers/mapToolsHandler.ts +281 -0
- package/src/handlers/measureToolsHandler.ts +424 -0
- package/src/handlers/memorialButtonControl.ts +171 -0
- package/src/handlers/toolsConstants.ts +69 -0
- package/src/index.ts +6 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/types/index.ts +260 -0
- package/src/types/json.d.ts +4 -0
- package/src/types/shpjs.d.ts +7 -0
- package/src/types/svg.d.ts +4 -0
- package/src/utils/CoordinateConverter.ts +43 -0
- package/src/utils/areaCalculator.ts +9 -0
- package/src/utils/drawingPathOptions.ts +57 -0
- package/src/utils/geometryCalculator.ts +62 -0
- package/src/utils/geometryFileParseResult.ts +5 -0
- package/src/utils/memorialLayer.ts +7 -0
- package/src/utils/menuHistory.ts +71 -0
- package/src/utils/normalizeGeoJsonToFeatures.ts +52 -0
- package/src/utils/parseGeoJsonFile.ts +23 -0
- package/src/utils/parseKmlFile.ts +31 -0
- package/src/utils/parseShapefileZip.ts +29 -0
- package/src/utils/stableMarker.ts +52 -0
- package/src/utils/validateImportedPolygonGeometry.ts +62 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FontAwesomeIcon :icon="['fas', iconName]" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
7
|
+
import {
|
|
8
|
+
faCheck,
|
|
9
|
+
faChevronLeft,
|
|
10
|
+
faChevronRight,
|
|
11
|
+
faXmark,
|
|
12
|
+
faTerminal,
|
|
13
|
+
faFileLines,
|
|
14
|
+
faUpload,
|
|
15
|
+
faPencil,
|
|
16
|
+
faTrash,
|
|
17
|
+
faChevronDown
|
|
18
|
+
} from '@fortawesome/free-solid-svg-icons'
|
|
19
|
+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
20
|
+
import { FaIconName } from '../../types'
|
|
21
|
+
|
|
22
|
+
type IconProps = {
|
|
23
|
+
iconName: FaIconName
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
defineProps<IconProps>()
|
|
27
|
+
|
|
28
|
+
library.add(
|
|
29
|
+
faCheck,
|
|
30
|
+
faXmark,
|
|
31
|
+
faChevronRight,
|
|
32
|
+
faChevronLeft,
|
|
33
|
+
faTerminal,
|
|
34
|
+
faFileLines,
|
|
35
|
+
faUpload,
|
|
36
|
+
faPencil,
|
|
37
|
+
faTrash,
|
|
38
|
+
faChevronDown
|
|
39
|
+
)
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="isLoading"
|
|
4
|
+
class="map-loading"
|
|
5
|
+
>
|
|
6
|
+
<img src="../../assets/icons/loading.gif" />
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
defineProps<{ isLoading: boolean }>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.map-container .map-loading {
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset: 0px;
|
|
18
|
+
backdrop-filter: blur(var(--mapa-size-base-2));
|
|
19
|
+
display: flex;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
align-items: center;
|
|
22
|
+
background-color: var(--mapa-base-backdrop);
|
|
23
|
+
z-index: 1500;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:id="mapOptions.config.id || 'map'"
|
|
4
|
+
style="height: inherit; width: inherit"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import L from 'leaflet'
|
|
10
|
+
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
11
|
+
import DrawingControlHandler from '../../handlers/drawingControl'
|
|
12
|
+
import MapHandler from '../../handlers/mapHandler'
|
|
13
|
+
import { DEFAULT_MAP_OPTIONS } from '../../handlers/constants'
|
|
14
|
+
import MapToolsHandler from '../../handlers/mapToolsHandler'
|
|
15
|
+
import { resolveMapToolsConfig } from '../../handlers/toolsConstants'
|
|
16
|
+
import { resolveDrawingPathOptions, type MemorialDrawShape } from '../../utils/drawingPathOptions'
|
|
17
|
+
import { bindMarkerZoomStability } from '../../utils/stableMarker'
|
|
18
|
+
import {
|
|
19
|
+
DrawingConfig,
|
|
20
|
+
DrawingEvent,
|
|
21
|
+
MapConfig,
|
|
22
|
+
MapConfigConfig,
|
|
23
|
+
MapLayers,
|
|
24
|
+
MapToolsConfig,
|
|
25
|
+
MeasureCompleteEvent,
|
|
26
|
+
MemorialConfig
|
|
27
|
+
} from '../../types'
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
(e: 'startLoading'): void
|
|
31
|
+
(e: 'stopLoading'): void
|
|
32
|
+
(e: 'onDrawing', data: DrawingEvent): void
|
|
33
|
+
(e: 'onFullscreenChange', active: boolean): void
|
|
34
|
+
(e: 'onMeasureComplete', data: MeasureCompleteEvent): void
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
type MapProps = {
|
|
38
|
+
mapOptions: MapConfig
|
|
39
|
+
layers: MapLayers
|
|
40
|
+
drawingOptions?: DrawingConfig
|
|
41
|
+
memorialOptions?: MemorialConfig
|
|
42
|
+
toolsOptions?: MapToolsConfig
|
|
43
|
+
fullscreenContainer?: HTMLElement | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = defineProps<MapProps>()
|
|
47
|
+
|
|
48
|
+
const map = ref<L.Map>()
|
|
49
|
+
const layerControl = ref<L.Control.Layers>()
|
|
50
|
+
const drawItemsGroup = ref<L.FeatureGroup>()
|
|
51
|
+
|
|
52
|
+
let mapHandlerInstance: MapHandler | null = null
|
|
53
|
+
let mapToolsHandler: MapToolsHandler | null = null
|
|
54
|
+
let unbindMarkerZoomStability: (() => void) | null = null
|
|
55
|
+
|
|
56
|
+
const setupMarkerZoomStability = (): void => {
|
|
57
|
+
unbindMarkerZoomStability?.()
|
|
58
|
+
unbindMarkerZoomStability = null
|
|
59
|
+
|
|
60
|
+
if (props.mapOptions?.config?.stabilizeMarkersOnZoom === false) return
|
|
61
|
+
if (!map.value || !drawItemsGroup.value) return
|
|
62
|
+
|
|
63
|
+
unbindMarkerZoomStability = bindMarkerZoomStability(map.value, drawItemsGroup.value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onMounted(async () => {
|
|
67
|
+
initMap()
|
|
68
|
+
if (props.drawingOptions?.show) handleDrawingControls()
|
|
69
|
+
await nextTick()
|
|
70
|
+
handleMapTools()
|
|
71
|
+
await nextTick()
|
|
72
|
+
mapToolsHandler?.alignTopRightControls()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
onBeforeUnmount(() => {
|
|
76
|
+
unbindMarkerZoomStability?.()
|
|
77
|
+
unbindMarkerZoomStability = null
|
|
78
|
+
mapToolsHandler?.destroy()
|
|
79
|
+
mapToolsHandler = null
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const initMap = (): void => {
|
|
83
|
+
const { config } = props.mapOptions
|
|
84
|
+
const mapConfig: MapConfigConfig = {
|
|
85
|
+
...DEFAULT_MAP_OPTIONS,
|
|
86
|
+
...config,
|
|
87
|
+
...(resolveMapToolsConfig(props.toolsOptions) ? { zoomControl: false } : {})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
mapHandlerInstance = new MapHandler(mapConfig)
|
|
91
|
+
|
|
92
|
+
const emitterCallback = (eventName: string) => {
|
|
93
|
+
if (eventName === 'startLoading') emit('startLoading')
|
|
94
|
+
if (eventName === 'stopLoading') emit('stopLoading')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
mapHandlerInstance.init(props.layers, emitterCallback)
|
|
98
|
+
|
|
99
|
+
map.value = mapHandlerInstance.map
|
|
100
|
+
layerControl.value = mapHandlerInstance.layerControl
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const resolveFullscreenContainer = (): HTMLElement | null => {
|
|
104
|
+
if (props.fullscreenContainer) return props.fullscreenContainer
|
|
105
|
+
const fromMap = map.value?.getContainer()?.closest('.map-container')
|
|
106
|
+
return fromMap instanceof HTMLElement ? fromMap : null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const handleMapTools = (): void => {
|
|
110
|
+
const fullscreenContainer = resolveFullscreenContainer()
|
|
111
|
+
if (!resolveMapToolsConfig(props.toolsOptions) || !map.value || !fullscreenContainer) return
|
|
112
|
+
|
|
113
|
+
mapToolsHandler = new MapToolsHandler(
|
|
114
|
+
map.value,
|
|
115
|
+
props.toolsOptions,
|
|
116
|
+
mapHandlerInstance!.initialView,
|
|
117
|
+
drawItemsGroup.value ?? null,
|
|
118
|
+
fullscreenContainer,
|
|
119
|
+
{
|
|
120
|
+
onFullscreenChange: (active) => emit('onFullscreenChange', active),
|
|
121
|
+
onMeasureComplete: (data) => emit('onMeasureComplete', data)
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
;(map.value as L.Map & { alignTopRightControls?: () => void }).alignTopRightControls = () =>
|
|
126
|
+
mapToolsHandler?.alignTopRightControls()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleDrawingControls = (): void => {
|
|
130
|
+
drawItemsGroup.value = new L.FeatureGroup()
|
|
131
|
+
drawItemsGroup.value.addTo(map.value!)
|
|
132
|
+
|
|
133
|
+
const drawingControlHandler = new DrawingControlHandler(map.value!, drawItemsGroup.value, props.drawingOptions)
|
|
134
|
+
|
|
135
|
+
map.value!.pm.addControls(drawingControlHandler.options)
|
|
136
|
+
|
|
137
|
+
drawingControlHandler.handleDrawingEvents((data: DrawingEvent) => {
|
|
138
|
+
emit('onDrawing', data)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
drawItemsGroup.value = drawingControlHandler.drawItemsGroup
|
|
142
|
+
map.value = drawingControlHandler.map
|
|
143
|
+
|
|
144
|
+
mapToolsHandler?.setDrawItemsGroup(drawItemsGroup.value)
|
|
145
|
+
setupMarkerZoomStability()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const centerMap = (): void => {
|
|
149
|
+
mapToolsHandler?.centerMap()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const enterFullscreen = (): void => {
|
|
153
|
+
mapToolsHandler?.enterFullscreen()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const exitFullscreen = (): void => {
|
|
157
|
+
mapToolsHandler?.exitFullscreen()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const toggleFullscreen = (): void => {
|
|
161
|
+
mapToolsHandler?.toggleFullscreen()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const toggleMeasureArea = (): void => {
|
|
165
|
+
mapToolsHandler?.toggleMeasureArea()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const getDrawingPathOptions = (shape: MemorialDrawShape) =>
|
|
169
|
+
resolveDrawingPathOptions(map.value, props.drawingOptions, shape)
|
|
170
|
+
|
|
171
|
+
defineExpose({
|
|
172
|
+
map,
|
|
173
|
+
layerControl,
|
|
174
|
+
drawItemsGroup,
|
|
175
|
+
leaflet: L,
|
|
176
|
+
getDrawingPathOptions,
|
|
177
|
+
centerMap,
|
|
178
|
+
enterFullscreen,
|
|
179
|
+
exitFullscreen,
|
|
180
|
+
toggleFullscreen,
|
|
181
|
+
toggleMeasureArea
|
|
182
|
+
})
|
|
183
|
+
</script>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ElMenuItem
|
|
3
|
+
:index="data.key"
|
|
4
|
+
class="child-menu"
|
|
5
|
+
>
|
|
6
|
+
<template #title>
|
|
7
|
+
<div class="child-menu-row">
|
|
8
|
+
<div class="child-layer-title">
|
|
9
|
+
<span
|
|
10
|
+
:style="{
|
|
11
|
+
borderColor: data.style.fillColor,
|
|
12
|
+
backgroundColor: data.style.color
|
|
13
|
+
}"
|
|
14
|
+
class="child-layer-legend"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<span>{{ data.name }}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<span class="switch-component">
|
|
20
|
+
<ElSwitch
|
|
21
|
+
v-model="active"
|
|
22
|
+
@click.stop
|
|
23
|
+
>
|
|
24
|
+
<template #active-action>
|
|
25
|
+
<FontAwesomeIcon iconName="check" />
|
|
26
|
+
</template>
|
|
27
|
+
<template #inactive-action>
|
|
28
|
+
<FontAwesomeIcon iconName="xmark" />
|
|
29
|
+
</template>
|
|
30
|
+
</ElSwitch>
|
|
31
|
+
<span class="child-layer-status">
|
|
32
|
+
{{ active ? data.toggle.active : data.toggle.inactive }}
|
|
33
|
+
</span>
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
</ElMenuItem>
|
|
38
|
+
<ElDivider class="child-menu-divider-row" />
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<script lang="ts" setup>
|
|
42
|
+
import { ElDivider, ElMenuItem, ElSwitch } from 'element-plus'
|
|
43
|
+
import { computed } from 'vue'
|
|
44
|
+
import FontAwesomeIcon from '../fa-icon/FontAwesomeIcon.vue'
|
|
45
|
+
import { LayerData } from '../../types'
|
|
46
|
+
import { setHistory } from '../../utils/menuHistory.ts'
|
|
47
|
+
|
|
48
|
+
type ChildMenuProps = {
|
|
49
|
+
data: LayerData
|
|
50
|
+
persist: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const props = defineProps<ChildMenuProps>()
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits<{ onChildLayerToggle: [LayerData] }>()
|
|
56
|
+
|
|
57
|
+
const active = computed<boolean>({
|
|
58
|
+
get: () => props.data.active,
|
|
59
|
+
set: (active: boolean) => {
|
|
60
|
+
toggleLayerVisible(active)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const toggleLayerVisible = (active: boolean): void => {
|
|
65
|
+
const layer = { ...props.data, active }
|
|
66
|
+
if (props.persist) setHistory(layer)
|
|
67
|
+
emit('onChildLayerToggle', layer)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<style>
|
|
73
|
+
.child-menu {
|
|
74
|
+
padding-left: var(--mapa-size-base-20) !important;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.child-menu .child-menu-row {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
gap: var(--mapa-size-base-10);
|
|
81
|
+
width: 100%;
|
|
82
|
+
font-size: var(--mapa-fs-12);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.child-menu .switch-component {
|
|
86
|
+
display: flex;
|
|
87
|
+
justify-content: center;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: var(--mapa-size-base-5);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.child-menu .child-layer-title {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: var(--mapa-size-base-5);
|
|
96
|
+
word-wrap: break-word;
|
|
97
|
+
white-space: normal;
|
|
98
|
+
line-height: 150%;
|
|
99
|
+
margin-block: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.child-menu .child-layer-legend {
|
|
103
|
+
display: inline-block;
|
|
104
|
+
width: var(--mapa-size-base-8);
|
|
105
|
+
height: var(--mapa-size-base-8);
|
|
106
|
+
min-width: var(--mapa-size-base-8);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.child-menu-divider-row {
|
|
110
|
+
margin: 0 !important;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="customClasses.layerMenu">
|
|
3
|
+
<ElButton
|
|
4
|
+
v-if="!props.options?.removeMenu"
|
|
5
|
+
:class="customClasses.menuButton"
|
|
6
|
+
@click="isMenuOpen = !isMenuOpen"
|
|
7
|
+
>
|
|
8
|
+
<FontAwesomeIcon :iconName="iconButton" />
|
|
9
|
+
</ElButton>
|
|
10
|
+
<ElMenu
|
|
11
|
+
:class="customClasses.customMenu"
|
|
12
|
+
mode="vertical"
|
|
13
|
+
>
|
|
14
|
+
<div id="external-id-top-menu"></div>
|
|
15
|
+
<template
|
|
16
|
+
v-for="group in props.layersConfig"
|
|
17
|
+
:key="group.key"
|
|
18
|
+
>
|
|
19
|
+
<ParentMenu
|
|
20
|
+
:groupData="group"
|
|
21
|
+
:persist="!!props.options?.persist"
|
|
22
|
+
@onChildLayerToggle="onChildLayerChange"
|
|
23
|
+
@onGroupLayerToggle="onGroupLayerToggle"
|
|
24
|
+
/>
|
|
25
|
+
</template>
|
|
26
|
+
<div id="external-id-bottom-menu"></div>
|
|
27
|
+
</ElMenu>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script lang="ts" setup>
|
|
32
|
+
import { ElButton, ElMenu } from 'element-plus'
|
|
33
|
+
import L from 'leaflet'
|
|
34
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
35
|
+
import FontAwesomeIcon from '../fa-icon/FontAwesomeIcon.vue'
|
|
36
|
+
import ParentMenu from './ParentMenu.vue'
|
|
37
|
+
import { GroupLayerData, LayerData, LayersConfig, LayersMenuConfig } from '../../types'
|
|
38
|
+
import { resolveLayerActiveState, shouldInitLayerOnMap } from '../../utils/menuHistory.ts'
|
|
39
|
+
|
|
40
|
+
type MenuProps = {
|
|
41
|
+
layersConfig: LayersConfig
|
|
42
|
+
options?: LayersMenuConfig
|
|
43
|
+
map: L.Map
|
|
44
|
+
layerControl?: L.Control.Layers
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ConvertedLayers = {
|
|
48
|
+
[key: string]: L.TileLayer
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ConvertedGeoJsonLayers = {
|
|
52
|
+
[key: string]: L.GeoJSON
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits<{
|
|
56
|
+
(e: 'startLoading'): void
|
|
57
|
+
(e: 'stopLoading'): void
|
|
58
|
+
(e: 'onChildLayerToggle', data: LayerData): void
|
|
59
|
+
(e: 'onGroupLayerToggle', data: GroupLayerData): void
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
const props = defineProps<MenuProps>()
|
|
63
|
+
|
|
64
|
+
const isMenuOpen = ref<boolean>(false)
|
|
65
|
+
const convertedLayers = ref<ConvertedLayers>({})
|
|
66
|
+
const convertedGeoJsonLayers = ref<ConvertedGeoJsonLayers>({})
|
|
67
|
+
|
|
68
|
+
type CustomClasses = {
|
|
69
|
+
layerMenu: string
|
|
70
|
+
menuButton: string
|
|
71
|
+
customMenu: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const customClasses = computed((): CustomClasses => {
|
|
75
|
+
const status = isMenuOpen.value ? 'open' : 'close'
|
|
76
|
+
return {
|
|
77
|
+
layerMenu: `layer-menu layer-menu-${props.options?.size || 'medium'}`,
|
|
78
|
+
menuButton: `map-menu-button map-menu-button-${status}`,
|
|
79
|
+
customMenu: `map-custom-menu map-custom-menu-${status}`
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
type IconButton = 'chevron-left' | 'chevron-right'
|
|
84
|
+
|
|
85
|
+
const iconButton = computed((): IconButton => {
|
|
86
|
+
return isMenuOpen.value ? 'chevron-left' : 'chevron-right'
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const onInitDefaultLayer = (layer: LayerData): void => {
|
|
90
|
+
if (layer.geojson) return handleGeoJsonLayer(layer)
|
|
91
|
+
|
|
92
|
+
handleWmsLayer(layer)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const initializedLayerKeys = new Set<string>()
|
|
96
|
+
|
|
97
|
+
const initDefaultLayers = (): void => {
|
|
98
|
+
if (!props.map) return
|
|
99
|
+
|
|
100
|
+
const persist = !!props.options?.persist
|
|
101
|
+
|
|
102
|
+
props.layersConfig?.forEach((group) => {
|
|
103
|
+
group.layers?.forEach((layer) => {
|
|
104
|
+
if (initializedLayerKeys.has(layer.key)) return
|
|
105
|
+
if (!shouldInitLayerOnMap(layer, persist)) return
|
|
106
|
+
|
|
107
|
+
const resolved = resolveLayerActiveState(layer, persist)
|
|
108
|
+
onInitDefaultLayer(resolved)
|
|
109
|
+
initializedLayerKeys.add(layer.key)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
watch(
|
|
115
|
+
[() => props.layersConfig, () => props.map],
|
|
116
|
+
(newValues, oldValues) => {
|
|
117
|
+
const [, newMap] = newValues
|
|
118
|
+
const [, oldMap] = oldValues ?? []
|
|
119
|
+
if (newMap !== oldMap) {
|
|
120
|
+
initializedLayerKeys.clear()
|
|
121
|
+
}
|
|
122
|
+
void nextTick(() => {
|
|
123
|
+
initDefaultLayers()
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
{ immediate: true, deep: true }
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const onChildLayerChange = (layer: LayerData): void => {
|
|
130
|
+
if (layer.geojson) {
|
|
131
|
+
handleGeoJsonLayer(layer)
|
|
132
|
+
} else {
|
|
133
|
+
handleWmsLayer(layer)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
emit('onChildLayerToggle', layer)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const onGroupLayerToggle = (parent: GroupLayerData): void => {
|
|
140
|
+
parent.layers.forEach((childLayer: LayerData) => {
|
|
141
|
+
if (childLayer.geojson) {
|
|
142
|
+
handleGeoJsonLayer(childLayer)
|
|
143
|
+
} else {
|
|
144
|
+
handleWmsLayer(childLayer)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
emit('onGroupLayerToggle', parent)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const convertToWmsLayer = (layer: LayerData): L.TileLayer => {
|
|
152
|
+
const wmsLayer = L.tileLayer.wms(layer.baseUrl, {
|
|
153
|
+
layers: layer.layers,
|
|
154
|
+
format: layer.format || 'image/png',
|
|
155
|
+
transparent: layer.transparent
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
if (layer?.cqlFilter && layer.cqlFilter.length > 0) {
|
|
159
|
+
wmsLayer.setParams({
|
|
160
|
+
cql_filter: layer.cqlFilter
|
|
161
|
+
} as any)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
watchLayerStatus(wmsLayer)
|
|
165
|
+
|
|
166
|
+
return wmsLayer
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleWmsLayer = (layer: LayerData): void => {
|
|
170
|
+
if (convertedLayers.value[layer.key]) {
|
|
171
|
+
if (!layer.active) return removeWmsLayer(layer)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!layer.active) return
|
|
175
|
+
|
|
176
|
+
const wmsLayer = convertToWmsLayer(layer)
|
|
177
|
+
convertedLayers.value[layer.key] = wmsLayer
|
|
178
|
+
|
|
179
|
+
wmsLayer.addTo(props.map)
|
|
180
|
+
|
|
181
|
+
if (props.layerControl) {
|
|
182
|
+
props.layerControl.addOverlay(wmsLayer, `${wmsLayer.options.attribution}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const removeWmsLayer = (layer: LayerData): void => {
|
|
187
|
+
const wmsLayer = convertedLayers.value[layer.key]
|
|
188
|
+
if (!wmsLayer) return
|
|
189
|
+
|
|
190
|
+
props.map.removeLayer(wmsLayer)
|
|
191
|
+
props.layerControl?.removeLayer(wmsLayer)
|
|
192
|
+
|
|
193
|
+
delete convertedLayers.value[layer.key]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const watchLayerStatus = (tileLayer: L.TileLayer): void => {
|
|
197
|
+
tileLayer.on('loading', () => {
|
|
198
|
+
emit('startLoading')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
tileLayer.on('load', () => {
|
|
202
|
+
emit('stopLoading')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
tileLayer.on('error', () => {
|
|
206
|
+
emit('stopLoading')
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const convertToGeojsonLayer = (layer: LayerData): L.GeoJSON => {
|
|
211
|
+
const geojsonLayer = L.geoJSON(layer.geojson, {
|
|
212
|
+
...layer,
|
|
213
|
+
style: layer.style
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
watchGeoJsonLayerStatus(geojsonLayer)
|
|
217
|
+
|
|
218
|
+
return geojsonLayer
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const handleGeoJsonLayer = (layer: LayerData): void => {
|
|
222
|
+
if (convertedGeoJsonLayers.value[layer.key]) {
|
|
223
|
+
if (!layer.active) return removeGeoJsonLayer(layer)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!layer.active) return
|
|
227
|
+
|
|
228
|
+
const geoJsonLayer = convertToGeojsonLayer(layer)
|
|
229
|
+
convertedGeoJsonLayers.value[layer.key] = geoJsonLayer
|
|
230
|
+
|
|
231
|
+
geoJsonLayer.addTo(props.map)
|
|
232
|
+
|
|
233
|
+
if (props.layerControl) {
|
|
234
|
+
props.layerControl.addOverlay(geoJsonLayer, `${geoJsonLayer.options.attribution}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const removeGeoJsonLayer = (layer: LayerData): void => {
|
|
239
|
+
const geoJsonLayer = convertedGeoJsonLayers.value[layer.key]
|
|
240
|
+
if (!geoJsonLayer) return
|
|
241
|
+
|
|
242
|
+
props.map.removeLayer(geoJsonLayer)
|
|
243
|
+
props.layerControl?.removeLayer(geoJsonLayer)
|
|
244
|
+
|
|
245
|
+
delete convertedGeoJsonLayers.value[layer.key]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const watchGeoJsonLayerStatus = (geoJsonLayer: L.GeoJSON): void => {
|
|
249
|
+
geoJsonLayer.on('loading', () => {
|
|
250
|
+
emit('startLoading')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
geoJsonLayer.on('load', () => {
|
|
254
|
+
emit('stopLoading')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
geoJsonLayer.on('error', () => {
|
|
258
|
+
emit('stopLoading')
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
<style>
|
|
264
|
+
.layer-menu > .map-custom-menu,
|
|
265
|
+
.layer-menu > .map-menu-button {
|
|
266
|
+
position: absolute;
|
|
267
|
+
top: 50%;
|
|
268
|
+
left: -50px;
|
|
269
|
+
transform: translateY(-50%);
|
|
270
|
+
z-index: 1001;
|
|
271
|
+
transition: var(--mapa-base-transition);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.layer-menu > .map-custom-menu {
|
|
275
|
+
box-shadow: var(--mapa-base-box-shadow);
|
|
276
|
+
padding: var(--mapa-size-base-10);
|
|
277
|
+
overflow-y: scroll;
|
|
278
|
+
scrollbar-width: none;
|
|
279
|
+
height: 100%;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.layer-menu > .map-menu-button {
|
|
283
|
+
height: var(--mapa-size-base-40);
|
|
284
|
+
width: var(--mapa-size-base-40);
|
|
285
|
+
border-radius: 0 var(--mapa-size-base-20) var(--mapa-size-base-20) 0;
|
|
286
|
+
color: var(--mapa-base-green);
|
|
287
|
+
|
|
288
|
+
&:hover {
|
|
289
|
+
background-color: var(--mapa-base-white);
|
|
290
|
+
color: var(--mapa-base-green);
|
|
291
|
+
border-color: var(--mapa-base-white);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
</style>
|