Skip to content

Commit a30508a

Browse files
authored
Merge pull request #602 from geoadmin/feat_PB-110_parse_print_capabilities
PB-110 : add print capabilities parsing and menu section
2 parents 09e32e5 + 5bbbc5e commit a30508a

File tree

7 files changed

+326
-9
lines changed

7 files changed

+326
-9
lines changed

src/api/__tests__/print.api.spec.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from 'chai'
2+
import { describe, it } from 'vitest'
3+
4+
import { PrintLayout, PrintLayoutAttribute } from '@/api/print.api.js'
5+
6+
describe('Print API unit tests', () => {
7+
describe('PrintLayoutAttribute tests', () => {
8+
it('Correctly tells that a param is invalid', () => {
9+
const testInstance = new PrintLayoutAttribute('test', 'String')
10+
expect(testInstance.isValid).to.be.false
11+
})
12+
it('Tells an attribute is valid if it has a default value', () => {
13+
const testInstance = new PrintLayoutAttribute('test', 'String', 'default')
14+
expect(testInstance.isValid).to.be.true
15+
})
16+
it('returns an empty array for scales if no clientInfo is defined', () => {
17+
const testInstance = new PrintLayoutAttribute('test', 'String')
18+
expect(testInstance.scales).to.be.an('Array').lengthOf(0)
19+
})
20+
it('returns the scales defined in clientInfo', () => {
21+
const scales = [1, 2, 3, 4]
22+
const testInstance = new PrintLayoutAttribute('test', 'String', null, null, {
23+
scales,
24+
})
25+
expect(testInstance.scales).to.eql(scales)
26+
})
27+
})
28+
describe('PrintLayout tests', () => {
29+
it('Filters out invalid attributes inputs', () => {
30+
const testInstance = new PrintLayout('test', null, '', undefined, 0)
31+
expect(testInstance.attributes).to.be.an('Array').lengthOf(0)
32+
})
33+
it('Correctly tells if all attributes are valid', () => {
34+
const attributeOne = new PrintLayoutAttribute('one', 'Number')
35+
const attributeTwo = new PrintLayoutAttribute('two', 'String', 'default value')
36+
const testInstance = new PrintLayout('test', attributeOne, attributeTwo)
37+
// attribute one requires a value
38+
expect(testInstance.isReadyToPrint).to.be.false
39+
40+
attributeOne.value = 123
41+
expect(testInstance.isReadyToPrint).to.be.true
42+
})
43+
it('gets the scales from the "map" attribute correctly', () => {
44+
const attributeNotMapWithScales = new PrintLayoutAttribute(
45+
'not map',
46+
'String',
47+
null,
48+
null,
49+
{ scales: [1, 2, 3] }
50+
)
51+
// testing param that aren't "map" but have a scale
52+
const scalesButNotInMapAttr = new PrintLayout('wrong', attributeNotMapWithScales)
53+
expect(scalesButNotInMapAttr.scales).to.be.an('Array').lengthOf(0)
54+
55+
const scales = [5, 6, 7]
56+
const scalesInMapAttr = new PrintLayout(
57+
'correct',
58+
// also adding the wrong attribute here, it should be ignored when gathering scales
59+
attributeNotMapWithScales,
60+
new PrintLayoutAttribute('map', null, null, null, { scales })
61+
)
62+
expect(scalesInMapAttr.scales).to.eql(scales)
63+
})
64+
})
65+
})

src/api/print.api.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import axios from 'axios'
2+
3+
import { API_SERVICES_BASE_URL } from '@/config.js'
4+
import log from '@/utils/logging.js'
5+
6+
/**
7+
* One parameter required to start a print job.
8+
*
9+
* This is where information about print capabilities for a specific layout will be stored (inside
10+
* the clientInfo object), e.g. which scales can be used on the map, available DPIs, etc...
11+
*/
12+
export class PrintLayoutAttribute {
13+
constructor(name, type, defaultValue = null, clientParams = null, clientInfo = null) {
14+
this.name = name
15+
this.type = type
16+
this.defaultValue = defaultValue
17+
this.clientParams = clientParams
18+
this.clientInfo = clientInfo
19+
this.value = defaultValue
20+
}
21+
22+
/**
23+
* Flag telling of this layout attribute must be filled before sending a print job with its
24+
* layout
25+
*
26+
* @returns {boolean}
27+
*/
28+
get isRequired() {
29+
return this.defaultValue === null
30+
}
31+
32+
/**
33+
* Flag telling if this layout attribute is valid, and ready to be sent to the backend
34+
*
35+
* @returns {boolean}
36+
*/
37+
get isValid() {
38+
return this.defaultValue !== null || this.value !== null
39+
}
40+
41+
/**
42+
* Returns all the scales defined in this attribute, if a clientInfo object is present. It will
43+
* return an empty array if no clientInfo is defined.
44+
*
45+
* @returns {Number[]}
46+
*/
47+
get scales() {
48+
return this.clientInfo?.scales || []
49+
}
50+
}
51+
52+
/** Representation of a layout available to be printed on our service-print3 backend */
53+
export class PrintLayout {
54+
/**
55+
* @param {String} name
56+
* @param {PrintLayoutAttribute} attributes
57+
*/
58+
constructor(name, ...attributes) {
59+
this.name = name
60+
this.attributes = attributes.filter(
61+
(attribute) => attribute instanceof PrintLayoutAttribute
62+
)
63+
}
64+
65+
/**
66+
* Flag telling of this print layout can be sent to the backend. Meaning all its attributes have
67+
* a valid value.
68+
*
69+
* @returns {boolean}
70+
*/
71+
get isReadyToPrint() {
72+
return !this.attributes.some((attribute) => !attribute.isValid)
73+
}
74+
75+
/**
76+
* Will returns all scales defined in the "map" attribute. Will return an empty array if no
77+
* "map" attribute is found, or if it doesn't contain any scale.
78+
*
79+
* @returns {Number[]}
80+
*/
81+
get scales() {
82+
return this.attributes.find((attribute) => attribute.name === 'map')?.scales || []
83+
}
84+
}
85+
86+
/** @returns Promise<PrintLayout[]> */
87+
export function readPrintCapabilities() {
88+
return new Promise((resolve, reject) => {
89+
axios
90+
.get(`${API_SERVICES_BASE_URL}print3/print/default/capabilities.json`)
91+
.then((response) => response.data)
92+
.then((capabilities) => {
93+
resolve(
94+
capabilities?.layouts.map((layout) => {
95+
return new PrintLayout(
96+
layout.name,
97+
layout.attributes.map((attribute) => {
98+
return new PrintLayoutAttribute(
99+
attribute.name,
100+
attribute.type,
101+
attribute.default,
102+
attribute.clientParams,
103+
attribute.clientInfo
104+
)
105+
})
106+
)
107+
})
108+
)
109+
})
110+
.catch((error) => {
111+
log.error('Error while loading print capabilities', error)
112+
reject(error)
113+
})
114+
})
115+
}

src/modules/menu/components/menu/MenuTray.vue

+13-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
<MenuSettings />
1414
</MenuSection>
1515
<MenuShareSection ref="shareSection" @open-menu-section="onOpenMenuSection" />
16+
<MenuPrintSection
17+
v-if="!is3dMode"
18+
ref="printSection"
19+
@open-menu-section="onOpenMenuSection"
20+
/>
1621
<!-- Drawing section is a glorified button, we always keep it closed and listen to click events -->
1722
<div id="drawSectionTooltip" tabindex="0">
1823
<MenuSection
@@ -67,11 +72,13 @@ import MenuActiveLayersList from '@/modules/menu/components/activeLayers/MenuAct
6772
import MenuAdvancedToolsList from '@/modules/menu/components/advancedTools/MenuAdvancedToolsList.vue'
6873
import MenuSection from '@/modules/menu/components/menu/MenuSection.vue'
6974
import MenuSettings from '@/modules/menu/components/menu/MenuSettings.vue'
75+
import MenuPrintSection from '@/modules/menu/components/print/MenuPrintSection.vue'
7076
import MenuShareSection from '@/modules/menu/components/share/MenuShareSection.vue'
7177
import MenuTopicSection from '@/modules/menu/components/topics/MenuTopicSection.vue'
7278
7379
export default {
7480
components: {
81+
MenuPrintSection,
7582
MenuShareSection,
7683
MenuTopicSection,
7784
MenuSection,
@@ -96,7 +103,12 @@ export default {
96103
/* Please note that if the following 2 arrays are updated, "grid-template-rows" in
97104
the css section must also be updated. */
98105
scrollableMenuSections: ['topicsSection', 'activeLayersSection'],
99-
nonScrollableMenuSections: ['settingsSection', 'shareSection', 'toolsSection'],
106+
nonScrollableMenuSections: [
107+
'settingsSection',
108+
'shareSection',
109+
'toolsSection',
110+
'printSection',
111+
],
100112
}
101113
},
102114
computed: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<script setup>
2+
import { computed, ref, watch } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { useStore } from 'vuex'
5+
6+
import MenuSection from '@/modules/menu/components/menu/MenuSection.vue'
7+
import { formatThousand } from '@/utils/numberUtils.js'
8+
9+
const emits = defineEmits(['openMenuSection'])
10+
11+
const isSectionShown = ref(false)
12+
const selectedLayoutName = ref(null)
13+
const selectedScale = ref(null)
14+
15+
const i18n = useI18n()
16+
const store = useStore()
17+
const printLayouts = computed(() => store.state.print.layouts)
18+
const selectedLayout = computed(() =>
19+
printLayouts.value.find((layout) => layout.name === selectedLayoutName.value)
20+
)
21+
const scales = computed(() => selectedLayout.value?.scales || [])
22+
23+
watch(printLayouts, () => {
24+
// whenever layouts are loaded form the backend, we select the first one as default value
25+
if (printLayouts.value.length > 0) {
26+
selectLayout(printLayouts.value[0])
27+
}
28+
})
29+
30+
function togglePrintMenu() {
31+
// load print layouts from the backend if they were not yet loaded
32+
if (printLayouts.value.length === 0) {
33+
store.dispatch('loadPrintLayouts')
34+
} else {
35+
// if layouts are already present, we select the first one as default value
36+
selectLayout(printLayouts.value[0])
37+
}
38+
isSectionShown.value = true
39+
}
40+
function selectLayout(layout) {
41+
selectedLayoutName.value = layout.name
42+
selectedScale.value = layout.scales[0]
43+
}
44+
45+
function close() {
46+
isSectionShown.value = false
47+
}
48+
49+
defineExpose({
50+
close,
51+
})
52+
</script>
53+
54+
<template>
55+
<MenuSection
56+
id="printSection"
57+
:title="$t('print')"
58+
:show-content="isSectionShown"
59+
data-cy="menu-print-section"
60+
secondary
61+
@click:header="togglePrintMenu"
62+
@open-menu-section="(id) => emits('openMenuSection', id)"
63+
>
64+
<div class="p-2 d-grid gap-2 menu-print-settings mx-4">
65+
<label for="print-layout-selector " class="col-form-label fw-bold me-2">{{
66+
i18n.t('print_layout')
67+
}}</label>
68+
<select id="print-layout-selector " v-model="selectedLayoutName" class="form-select">
69+
<option
70+
v-for="layout in printLayouts"
71+
:key="layout.name"
72+
:value="layout.name"
73+
@click="selectLayout(layout)"
74+
>
75+
{{ layout.name }}
76+
</option>
77+
</select>
78+
<label for="print-scale-selector " class="col-form-label fw-bold me-2">{{
79+
i18n.t('print_scale')
80+
}}</label>
81+
<select id="print-scale-selector " v-model="selectedScale" class="form-select">
82+
<option v-for="scale in scales" :key="scale" :value="scale">
83+
1:{{ formatThousand(scale) }}
84+
</option>
85+
</select>
86+
</div>
87+
</MenuSection>
88+
</template>
89+
90+
<style lang="scss" scoped>
91+
.menu-print-settings {
92+
grid-template-columns: 1fr 2fr;
93+
label {
94+
text-align: end;
95+
}
96+
}
97+
</style>

src/store/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import i18n from '@/store/modules/i18n.store'
1010
import layers from '@/store/modules/layers.store'
1111
import map from '@/store/modules/map.store'
1212
import position from '@/store/modules/position.store'
13+
import print from '@/store/modules/print.store'
1314
import search from '@/store/modules/search.store'
1415
import share from '@/store/modules/share.store'
1516
import topics from '@/store/modules/topics.store'
@@ -62,6 +63,7 @@ const store = createStore({
6263
ui,
6364
share,
6465
cesium,
66+
print,
6567
},
6668
})
6769

src/store/modules/print.store.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { readPrintCapabilities } from '@/api/print.api.js'
2+
import log from '@/utils/logging.js'
3+
4+
export default {
5+
state: {
6+
layouts: [],
7+
},
8+
getters: {},
9+
actions: {
10+
async loadPrintLayouts({ commit }) {
11+
try {
12+
const layouts = await readPrintCapabilities()
13+
commit('setPrintLayouts', [...layouts])
14+
} catch (error) {
15+
log.error('Error while loading print layouts', error)
16+
}
17+
},
18+
},
19+
mutations: {
20+
setPrintLayouts: (state, layouts) => (state.layouts = layouts),
21+
},
22+
}

0 commit comments

Comments
 (0)