Skip to content

Commit

Permalink
Merge pull request #533 from geoadmin/feat_BGDIINF_SB-3193_geoadmin_g…
Browse files Browse the repository at this point in the history
…roup_of_layers

BGDIINF_SB-3193 : topics are now group of layers
  • Loading branch information
pakb authored Nov 16, 2023
2 parents f18de91 + 14e93c3 commit 6534487
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 561 deletions.
26 changes: 26 additions & 0 deletions src/api/layers/GeoAdminGroupOfLayers.class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import GeoAdminLayer from '@/api/layers/GeoAdminLayer.class'
import LayerTypes from '@/api/layers/LayerTypes.enum'

/**
* Description of a group of layers, or category of layers, in the context of the catalogue. This
* group can't be added directly on the map, only its layer children can.
*
* This is used to describe categories in the topic navigation / representation.
*/
export default class GeoAdminGroupOfLayers extends GeoAdminLayer {
/**
* @param {String} id Unique identifier of this group of layer
* @param {String} name Name of this layer group, in the current i18n lang
* @param {GeoAdminLayer[]} layers Description of the layers being part of this group
*/
constructor(id, name, layers) {
super(name, LayerTypes.GROUP, id, id)
this.layers = [...layers]
}

clone() {
let clone = super.clone()
clone.layers = this.layers.map((layer) => layer.clone())
return clone
}
}
102 changes: 40 additions & 62 deletions src/api/topics.api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import GeoAdminGroupOfLayers from '@/api/layers/GeoAdminGroupOfLayers.class'
import { API_BASE_URL } from '@/config'
import {
getBackgroundLayerFromLegacyUrlParams,
Expand All @@ -15,7 +16,7 @@ export class Topic {
* @param {GeoAdminLayer} defaultBackgroundLayer The layer that should be activated as
* background layer by default when this topic is selected
* @param {GeoAdminLayer[]} layersToActivate All layers that should be added to the displayed
* layer (but not necessarily visible, that will depends on their state)
* layer (but not necessarily visible, that will depend on their state)
*/
constructor(id, backgroundLayers, defaultBackgroundLayer, layersToActivate) {
this.id = id
Expand All @@ -25,96 +26,73 @@ export class Topic {
}
}

/** @enum */
export const topicTypes = {
THEME: 'THEME',
LAYER: 'LAYER',
}

/**
* Element of a topic tree, can be a node (or theme) or a leaf (a layer)
*
* @abstract
*/
class TopicTreeItem {
/**
* @param {Number | String} id The ID of the node
* @param {String} name The name of this node translated in the current lang
* @param {topicTypes} type The type of this node, layer or theme
*/
constructor(id, name, type) {
this.id = id
this.name = name
this.type = type
}
}

/** Node of a topic three containing more themes and/or a list of layers */
export class TopicTreeTheme extends TopicTreeItem {
/**
* @param {Number} id The ID of this node
* @param {String} name The name of the theme, in the current lang
* @param {TopicTreeItem[]} children All the children of this node (can be either layers or
* themes, all mixed together)
*/
constructor(id, name, children) {
super(id, name, topicTypes.THEME)
this.children = [...children]
this.showChildren = false
}
}

/** A layer in the topic tree */
export class TopicTreeLayer extends TopicTreeItem {
/**
* @param {String} layerId The BOD layer ID of this layer
* @param {String} name The name of this layer in the current lang
*/
constructor(layerId, name) {
super(layerId, name, topicTypes.LAYER)
this.layerId = layerId
const gatherItemIdThatShouldBeOpened = (node) => {
const ids = []
node?.children?.forEach((child) => {
ids.push(...gatherItemIdThatShouldBeOpened(child))
})
if (node?.selectedOpen) {
ids.push(`${node.id}`)
}
return ids
}

/**
* Reads the output of the topic tree endpoint, and creates all themes and layers object accordingly
*
* @param {Object} node The node for whom we are looking into
* @returns {TopicTreeItem}
* @param {GeoAdminLayer[]} availableLayers All layers available from the layers' config
* @returns {GeoAdminLayer}
*/
const readTopicTreeRecursive = (node) => {
const readTopicTreeRecursive = (node, availableLayers) => {
if (node.category === 'topic') {
const children = []
node.children.forEach((topicChild) => {
children.push(readTopicTreeRecursive(topicChild))
try {
children.push(readTopicTreeRecursive(topicChild, availableLayers))
} catch (err) {
log.error(`Child ${topicChild.id} can't be loaded`, err)
}
})
return new TopicTreeTheme(node.id, node.label, children)
return new GeoAdminGroupOfLayers(`${node.id}`, node.label, children)
} else if (node.category === 'layer') {
return new TopicTreeLayer(node.layerBodId, node.label)
} else {
log.error('unknown topic node type', node.category)
return null
const matchingLayer = availableLayers.find(
(layer) => layer.serverLayerId === node.layerBodId || layer.getID() === node.layerBodId
)
if (matchingLayer) {
return matchingLayer
}
throw new Error(`Layer with BOD ID ${node.layerBodId} not found in the layers config`)
}
throw new Error(`unknown topic node type : ${node.category}`)
}

/**
* Loads the topic tree for a topic. This will be used to create the UI of the topic in the menu.
*
* @param {String} lang The lang in which to load the topic tree
* @param {Topic} topic The topic we want to load the topic tree
* @returns {Promise<TopicTreeItem[]>} A list of topic tree nodes
* @param {GeoAdminLayer[]} layersConfig All available layers for this app (the "layers config")
* @returns {Promise<{ layers: GeoAdminLayer[]; itemIdToOpen: String[] }>} A list of topic's layers
*/
export const loadTopicTreeForTopic = (lang, topic) => {
export const loadTopicTreeForTopic = (lang, topic, layersConfig) => {
return new Promise((resolve, reject) => {
axios
.get(`${API_BASE_URL}rest/services/${topic.id}/CatalogServer?lang=${lang}`)
.then((response) => {
const treeItems = []
const topicRoot = response.data.results.root
topicRoot.children.forEach((child) => {
treeItems.push(readTopicTreeRecursive(child))
try {
treeItems.push(readTopicTreeRecursive(child, layersConfig))
} catch (err) {
log.error(
`Error while loading Layer ${child.id} for Topic ${topic.id}`,
err
)
}
})
resolve(treeItems)
const itemIdToOpen = gatherItemIdThatShouldBeOpened(topicRoot)
resolve({ layers: treeItems, itemIdToOpen })
})
.catch((error) => {
reject(error)
Expand Down
35 changes: 35 additions & 0 deletions src/modules/menu/components/topics/LayerCatalogue.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup>
import LayerCatalogueItem from '@/modules/menu/components/topics/LayerCatalogueItem.vue'
import { defineProps } from 'vue'
import { useStore } from 'vuex'
const { layerCatalogue, compact } = defineProps({
layerCatalogue: {
type: Array,
required: true,
},
compact: {
type: Boolean,
default: false,
},
})
const store = useStore()
function clearPreviewLayer() {
if (store.state.layers.previewLayer) {
store.dispatch('clearPreviewLayer')
}
}
</script>

<template>
<div class="menu-topic-list" data-cy="menu-topic-tree" @mouseleave="clearPreviewLayer">
<LayerCatalogueItem
v-for="item in layerCatalogue"
:key="item.getID()"
:item="item"
:compact="compact"
/>
</div>
</template>
185 changes: 185 additions & 0 deletions src/modules/menu/components/topics/LayerCatalogueItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<script setup>
/**
* Node of a layer catalogue in the UI, rendering (and behavior) will differ if this is a group of
* layers or a single layer.
*/
import AbstractLayer from '@/api/layers/AbstractLayer.class'
import GeoAdminGroupOfLayers from '@/api/layers/GeoAdminGroupOfLayers.class'
import LayerLegendPopup from '@/modules/menu/components/LayerLegendPopup.vue'
import { ActiveLayerConfig } from '@/utils/layerUtils'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
// importing directly the vue component, see https://github.com/ivanvermeyen/vue-collapse-transition/issues/5
import CollapseTransition from '@ivanv/vue-collapse-transition/src/CollapseTransition.vue'
import { computed, defineProps, onMounted, ref, watch } from 'vue'
import { useStore } from 'vuex'
const { item, compact, depth } = defineProps({
item: {
type: AbstractLayer,
required: true,
},
compact: {
type: Boolean,
default: false,
},
depth: {
type: Number,
default: 0,
},
})
// Declaring own properties (ex-data)
const showChildren = ref(false)
const showLayerLegend = ref(false)
// Mapping the store to the component
const store = useStore()
const activeLayers = computed(() => store.state.layers.activeLayers)
const openThemesIds = computed(() => store.state.topics.openedTreeThemesIds)
const hasChildren = computed(() => item?.layers?.length > 0)
/**
* Flag telling if this layer can be added to the map (so if the UI should include the necessary
* element to do so)
*/
const canBeAddedToTheMap = computed(() => {
// only groups of layers from our backends can't be added to the map
return item && !(item instanceof GeoAdminGroupOfLayers)
})
const isPresentInActiveLayers = computed(() =>
activeLayers.value.find((layer) => layer.getID() === item.getID())
)
const isCurrentlyHidden = computed(
() =>
isPresentInActiveLayers.value &&
activeLayers.value.find((layer) => layer.getID() === item.getID() && !layer.visible)
)
// reacting to topic changes (some categories might need some auto-opening)
watch(openThemesIds, (newValue) => {
showChildren.value = showChildren.value || newValue.indexOf(item.getID()) !== -1
})
// reading the current topic at startup and opening any required category
onMounted(() => {
showChildren.value = openThemesIds.value.indexOf(item.getID()) !== -1
})
function startLayerPreview() {
if (canBeAddedToTheMap.value) {
store.dispatch('setPreviewLayer', item)
}
}
function onItemClick() {
if (hasChildren.value) {
showChildren.value = !showChildren.value
} else {
const matchingActiveLayer = store.getters.getActiveLayerById(item.getID())
if (matchingActiveLayer) {
store.dispatch('toggleLayerVisibility', matchingActiveLayer)
} else {
store.dispatch('addLayer', new ActiveLayerConfig(item.getID(), true))
}
}
}
</script>
<template>
<div class="menu-topic-item" data-cy="topic-tree-item">
<div
class="menu-topic-item-title"
:class="{ group: hasChildren }"
:data-cy="`topic-tree-item-${item.getID()}`"
@click="onItemClick"
@mouseenter="startLayerPreview"
>
<button
class="btn d-flex align-items-center"
:class="{
'text-danger': isPresentInActiveLayers || isCurrentlyHidden,
'btn-lg': !compact,
'btn-rounded': hasChildren,
}"
>
<FontAwesomeLayers v-if="hasChildren">
<FontAwesomeIcon class="text-secondary" icon="fa-regular fa-circle" />
<FontAwesomeIcon size="xs" :icon="showChildren ? 'minus' : 'plus'" />
</FontAwesomeLayers>
<FontAwesomeIcon
v-if="canBeAddedToTheMap"
:class="{
'ms-1': hasChildren,
}"
:icon="`far ${
isPresentInActiveLayers && !isCurrentlyHidden
? 'fa-check-square'
: 'fa-square'
}`"
/>
</button>
<span class="menu-topic-item-name">{{ item.name }}</span>
<button
v-if="canBeAddedToTheMap"
class="btn"
:class="{ 'btn-lg': !compact }"
data-cy="topic-tree-item-info"
@click.stop="showLayerLegend = true"
>
<FontAwesomeIcon icon="info-circle" />
</button>
</div>
<CollapseTransition :duration="200">
<ul
v-show="showChildren"
class="menu-topic-item-children"
:class="`ps-${2 + 2 * depth}`"
>
<LayerCatalogueItem
v-for="child in item.layers"
:key="`${child.getID()}`"
:item="child"
:depth="depth + 1"
:compact="compact"
/>
</ul>
</CollapseTransition>
<LayerLegendPopup
v-if="showLayerLegend"
:layer-id="item.getID()"
@close="showLayerLegend = false"
/>
</div>
</template>
<style lang="scss" scoped>
@import 'src/scss/webmapviewer-bootstrap-theme';
@import 'src/modules/menu/scss/menu-items';
.menu-topic-item {
border-bottom: none;
&-title {
@extend .menu-title;
cursor: pointer;
&.active {
color: $primary;
}
border-bottom-width: 1px;
border-bottom-color: $gray-400;
&.group {
border-bottom-style: solid;
}
&:not(.group) {
border-bottom-style: dashed;
}
}
}
.menu-topic-item-name {
@extend .menu-name;
}
.menu-topic-item-children {
@extend .menu-list;
}
</style>
Loading

0 comments on commit 6534487

Please sign in to comment.