Skip to content

Commit 8f68f70

Browse files
committed
wip: Provide a selection manager component (#1098)
1 parent 7b60eec commit 8f68f70

File tree

10 files changed

+406
-41
lines changed

10 files changed

+406
-41
lines changed

core/client/composables/selection.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { useStore } from './store.js'
44
export function useSelection (name, options = {}) {
55
// Item comparator
66
const comparator = options.matches || _.matches
7-
// data
7+
88
// selection store
99
const { store, set, get, has } = useStore(`selections.${name}`)
1010

1111
// functions
1212
// Single selection will rely on the lastly selected item only
1313
// Multiple selection mode will rely on all items
1414
function clearSelection () {
15+
if (!isSelectionEnabled()) return
1516
// Do not force an update if not required
1617
// We set a new array so that deeply watch is not required
1718
if (hasSelectedItem()) set('items', [])
@@ -28,13 +29,20 @@ export function useSelection (name, options = {}) {
2829
function isMultipleSelectionMode () {
2930
return get('mode') !== 'single'
3031
}
32+
function setSelectionEnabled (enabled = true) {
33+
return set('enabled', enabled)
34+
}
35+
function isSelectionEnabled () {
36+
return get('enabled')
37+
}
3138
function getSelectionFilter () {
3239
return get('filter')
3340
}
3441
function setSelectionFilter (filter) {
3542
return set('filter', filter)
3643
}
3744
function selectItem (item) {
45+
if (!isSelectionEnabled()) return
3846
const filter = getSelectionFilter()
3947
if (filter && !filter(item)) return
4048
const items = get('items')
@@ -43,6 +51,7 @@ export function useSelection (name, options = {}) {
4351
if (!selected) set('items', items.concat([item]))
4452
}
4553
function unselectItem (item) {
54+
if (!isSelectionEnabled()) return
4655
const items = get('items')
4756
// We set a new array so that deeply watch is not required
4857
_.remove(items, comparator(item))
@@ -66,6 +75,7 @@ export function useSelection (name, options = {}) {
6675
// We set a new array so that deeply watch is not required
6776
set('items', [])
6877
set('mode', 'single')
78+
set('enabled', true)
6979
}
7080

7181
// expose
@@ -76,6 +86,8 @@ export function useSelection (name, options = {}) {
7686
setSelectionMode,
7787
isSingleSelectionMode,
7888
isMultipleSelectionMode,
89+
setSelectionEnabled,
90+
isSelectionEnabled,
7991
getSelectionFilter,
8092
setSelectionFilter,
8193
selectItem,

map/client/cesium/utils/utils.style.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ function processStyle (style, feature, options, mappings) {
157157
// visibility attribute can be used to hide individual features
158158
// visibility is true by default but can also be a string when it's
159159
// a result of a lodash string template evaluation
160-
let visibility = _.get(style, `style.${type}.visibility`, true)
160+
let visibility = _.get(style, `style.${type}.visibility`, _.get(style, 'style.visibility', true))
161161
if (typeof visibility === 'string') visibility = visibility === 'true'
162162
// The 'kdk-hidden-features' pane is created when the leaflet map is initialized
163163
// if (!visibility) _.set(style, `style.${type}.pane`, 'kdk-hidden-features')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<template>
2+
<div class="fit column no-wrap">
3+
<div v-if="hasItems">
4+
<template v-for="(item, index) in items" :key="item._id">
5+
<div :class="rendererClass">
6+
<component
7+
:id="item._id"
8+
:item="item"
9+
:is="itemRenderer"
10+
v-bind="renderer"
11+
/>
12+
</div>
13+
</template>
14+
</div>
15+
<div v-else class="row justify-center q-pa-sm">
16+
<KStamp
17+
icon="las la-exclamation-circle"
18+
icon-size="sm"
19+
:text="$t('KFeaturesSelection.NONE_SELECTED')"
20+
direction="horizontal"
21+
/>
22+
</div>
23+
</div>
24+
</template>
25+
26+
<script setup>
27+
import { computed } from 'vue'
28+
import { loadComponent } from '../../../../core/client/utils'
29+
import KStamp from '../../../../core/client/components/KStamp.vue'
30+
import { useCurrentActivity } from '../../composables/activity.js'
31+
32+
// Props
33+
const props = defineProps({
34+
renderer: {
35+
type: Object,
36+
default: () => {
37+
return {
38+
component: 'selection/KSelectedLayerFeatures'
39+
}
40+
}
41+
}
42+
})
43+
44+
// Data
45+
const { getSelectedFeaturesByLayer } = useCurrentActivity()
46+
47+
// Computed
48+
const itemRenderer = computed(() => {
49+
return loadComponent(props.renderer.component)
50+
})
51+
const rendererClass = computed(() => {
52+
return props.renderer.class || 'q-pa-sm col-12 col-sm-6 col-md-4 col-lg-3'
53+
})
54+
const items = computed(() => {
55+
return getSelectedFeaturesByLayer()
56+
})
57+
const hasItems = computed(() => {
58+
return Object.keys(items.value).length > 0
59+
})
60+
61+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<template>
2+
<q-tree
3+
:nodes="[root]"
4+
node-key="_id"
5+
label-key="label"
6+
children-key="features"
7+
default-expand-all
8+
dense
9+
>
10+
<template v-slot:default-header="prop">
11+
<!-- Layer rendering -->
12+
<q-icon v-if="prop.node.icon" :name="prop.node.icon"/>
13+
<KLayerItem v-if="prop.node.name"
14+
v-bind="$props"
15+
:togglable="false"
16+
:layer="root"
17+
/>
18+
<!-- Features rendering -->
19+
<div v-else class="row fit items-center q-pl-md q-pr-sm no-wrap">
20+
<div :class="{
21+
'text-primary': root.isVisible,
22+
'text-grey-6': root.isDisabled || !root.isVisible
23+
}"
24+
>
25+
<span v-html="prop.node.label || prop.node._id" />
26+
</div>
27+
<q-space />
28+
<!-- Features actions -->
29+
<KPanel
30+
:id="`${prop.node.label}-feature-actions`"
31+
:content="featureActions"
32+
:context="prop.node"
33+
/>
34+
</div>
35+
</template>
36+
</q-tree>
37+
</template>
38+
39+
<script setup>
40+
import _ from 'lodash'
41+
import { Dialog } from 'quasar'
42+
import { ref, computed } from 'vue'
43+
import { useRoute, useRouter } from 'vue-router'
44+
import bbox from '@turf/bbox'
45+
import { Store, i18n } from '../../../../core/client'
46+
import KLayerItem from '../catalog/KLayerItem.vue'
47+
import { useCurrentActivity } from '../../composables/activity.js'
48+
import { getFeatureId, getFeatureLabel } from '../../utils/utils.js'
49+
import { isLayerDataEditable } from '../../utils/utils.layers.js'
50+
51+
// Props
52+
const props = defineProps({
53+
item: {
54+
type: Object,
55+
default: () => {}
56+
}
57+
})
58+
59+
// Data
60+
const route = useRoute()
61+
const router = useRouter()
62+
const { CurrentActivity } = useCurrentActivity()
63+
const editedFeatures = ref([])
64+
65+
// Computed
66+
const layerActions = computed(() => {
67+
return [{
68+
id: 'layer-actions',
69+
component: 'menu/KMenu',
70+
dropdownIcon: 'las la-ellipsis-v',
71+
actionRenderer: 'item',
72+
propagate: false,
73+
dense: true,
74+
content: [{
75+
id: 'zoom-to-selected-features',
76+
label: 'KSelectedLayerFeatures.ZOOM_TO_FEATURES_LABEL',
77+
icon: 'las la-search-location',
78+
handler: zoomToSelectedFeatures
79+
}, {
80+
id: 'edit-selected-features',
81+
label: 'KSelectedLayerFeatures.EDIT_FEATURES_LABEL',
82+
icon: 'las la-edit',
83+
handler: editSelectedFeatures,
84+
visible: isLayerDataEditable(props.item.layer)
85+
}, {
86+
id: 'remove-selected-features',
87+
label: 'KSelectedLayerFeatures.REMOVE_FEATURES_LABEL',
88+
icon: 'las la-trash',
89+
handler: removeSelectedFeatures,
90+
visible: isLayerDataEditable(props.item.layer)
91+
}]
92+
}]
93+
})
94+
const featureActions = computed(() => {
95+
return [{
96+
id: 'feature-actions',
97+
component: 'menu/KMenu',
98+
dropdownIcon: 'las la-ellipsis-v',
99+
actionRenderer: 'item',
100+
propagate: false,
101+
dense: true,
102+
content: [{
103+
id: 'zoom-to-selected-feature',
104+
label: 'KSelectedLayerFeatures.ZOOM_TO_FEATURE_LABEL',
105+
icon: 'las la-search-location',
106+
handler: zoomToSelectedFeature
107+
}, {
108+
id: 'edit-selected-feature',
109+
label: 'KSelectedLayerFeatures.EDIT_FEATURE_LABEL',
110+
icon: 'las la-edit',
111+
handler: editSelectedFeature,
112+
visible: isLayerDataEditable(props.item.layer)
113+
}, {
114+
id: 'edit-selected-feature-properties',
115+
label: 'KSelectedLayerFeatures.EDIT_FEATURE_PROPERTIES_LABEL',
116+
icon: 'las la-address-card',
117+
handler: editSelectedFeatureProperties,
118+
visible: isLayerDataEditable(props.item.layer) && _.get(props.item.layer, 'schema.content')
119+
}, {
120+
id: 'remove-selected-feature',
121+
label: 'KSelectedLayerFeatures.REMOVE_FEATURE_LABEL',
122+
icon: 'las la-trash',
123+
handler: removeSelectedFeature,
124+
visible: isLayerDataEditable(props.item.layer)
125+
}]
126+
}]
127+
})
128+
const root = computed(() => {
129+
const features = props.item.features.map(feature => Object.assign({
130+
label: getFeatureLabel(feature, props.item.layer),
131+
icon: (editedFeatures.value.contains(feature) ? 'las la-edit' : '')
132+
}, feature))
133+
// Replace default layer actions with new ones
134+
const root = Object.assign({
135+
icon: (editedFeatures.value.length > 0 ? 'las la-edit' : '')
136+
}, _.omit(props.item.layer, ['icon', 'actions']), { actions: layerActions.value, features })
137+
return root
138+
})
139+
140+
// Functions
141+
function zoomToSelectedFeatures () {
142+
CurrentActivity.value.zoomToBBox(bbox({ type: 'FeatureCollection', features: props.item.features }))
143+
}
144+
function zoomToSelectedFeature (feature) {
145+
CurrentActivity.value.zoomToBBox(bbox(feature))
146+
}
147+
function editSelectedFeatures () {
148+
// Zoom to then edit
149+
zoomToSelectedFeatures()
150+
CurrentActivity.value.startEditLayer(props.item.layer, {
151+
features: props.item.features.map(feature => getFeatureId(feature, props.item.layer)),
152+
editMode: 'edit-geometry',
153+
allowedEditModes: [
154+
'edit-properties',
155+
'edit-geometry',
156+
'drag',
157+
'rotate'
158+
],
159+
callback: (event) => {
160+
editedFeatures.value = (event.status === 'edit-start' ? props.item.features : [])
161+
}
162+
})
163+
}
164+
function editSelectedFeature (feature) {
165+
// Zoom to then edit
166+
zoomToSelectedFeature(feature)
167+
CurrentActivity.value.startEditLayer(props.item.layer, {
168+
features: [getFeatureId(feature, props.item.layer)],
169+
editMode: 'edit-geometry',
170+
allowedEditModes: [
171+
'edit-properties',
172+
'edit-geometry',
173+
'drag',
174+
'rotate'
175+
],
176+
callback: (event) => {
177+
editedFeatures.value = (event.status === 'edit-start' ? [feature] : [])
178+
}
179+
})
180+
}
181+
function editSelectedFeatureProperties (feature) {
182+
// Zoom to then edit
183+
zoomToSelectedFeature(feature)
184+
router.push({
185+
name: 'edit-map-layer-feature',
186+
query: route.query,
187+
params: Object.assign(route.params, {
188+
layerId: props.item.layer._id,
189+
layerName: props.item.layer.name,
190+
featureId: feature._id,
191+
contextId: Store.get('context')
192+
})
193+
})
194+
}
195+
function removeSelectedFeatures () {
196+
Dialog.create({
197+
title: i18n.t('KSelectedLayerFeatures.REMOVE_FEATURES_DIALOG_TITLE'),
198+
message: i18n.t('KSelectedLayerFeatures.REMOVE_FEATURES_DIALOG_MESSAGE'),
199+
html: true,
200+
ok: {
201+
label: i18n.t('OK'),
202+
flat: true
203+
},
204+
cancel: {
205+
label: i18n.t('CANCEL'),
206+
flat: true
207+
}
208+
}).onOk(async () => {
209+
await CurrentActivity.value.removeFeatures(props.item.features, props.item.layer)
210+
})
211+
}
212+
function removeSelectedFeature (feature) {
213+
Dialog.create({
214+
title: i18n.t('KSelectedLayerFeatures.REMOVE_FEATURE_DIALOG_TITLE'),
215+
message: i18n.t('KSelectedLayerFeatures.REMOVE_FEATURE_DIALOG_MESSAGE'),
216+
html: true,
217+
ok: {
218+
label: i18n.t('OK'),
219+
flat: true
220+
},
221+
cancel: {
222+
label: i18n.t('CANCEL'),
223+
flat: true
224+
}
225+
}).onOk(async () => {
226+
await CurrentActivity.value.removeFeatures(feature, props.item.layer)
227+
})
228+
}
229+
230+
</script>

0 commit comments

Comments
 (0)