Skip to content

Commit a5d9f1c

Browse files
committed
BGDIINF_SB-3009 : floating tooltip Cesium
BGDIINF_SB-3009 : floating tooltip Cesium
1 parent ddebf21 commit a5d9f1c

File tree

9 files changed

+584
-122
lines changed

9 files changed

+584
-122
lines changed
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<template>
2+
<div ref="mapPopover" class="map-popover" data-cy="popover" @contextmenu.stop>
3+
<div class="card">
4+
<div class="card-header d-flex">
5+
<span class="flex-grow-1 align-self-center">
6+
{{ title }}
7+
</span>
8+
<slot name="extra-buttons"></slot>
9+
<button
10+
v-if="authorizePrint"
11+
class="btn btn-sm btn-light d-flex align-items-center"
12+
@click="printContent"
13+
>
14+
<FontAwesomeIcon icon="print" />
15+
</button>
16+
<button
17+
class="btn btn-sm btn-light d-flex align-items-center"
18+
data-cy="map-popover-close-button"
19+
@click="onClose"
20+
>
21+
<FontAwesomeIcon icon="times" />
22+
</button>
23+
</div>
24+
<div
25+
id="mapPopoverContent"
26+
ref="mapPopoverContent"
27+
class="map-popover-content"
28+
:class="{ 'card-body': useContentPadding }"
29+
>
30+
<slot />
31+
</div>
32+
</div>
33+
</div>
34+
</template>
35+
36+
<script>
37+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
38+
import promptUserToPrintHtmlContent from '@/utils/print'
39+
40+
/** Map popover content and styles. Position handling is done in corresponding library components */
41+
export default {
42+
components: { FontAwesomeIcon },
43+
props: {
44+
authorizePrint: {
45+
type: Boolean,
46+
default: false,
47+
},
48+
title: {
49+
type: String,
50+
default: '',
51+
},
52+
useContentPadding: {
53+
type: Boolean,
54+
default: false,
55+
},
56+
},
57+
emits: ['close'],
58+
methods: {
59+
getMapPopoverRef() {
60+
return this.$refs.mapPopover
61+
},
62+
onClose() {
63+
this.$emit('close')
64+
},
65+
printContent() {
66+
promptUserToPrintHtmlContent('mapPopoverContent')
67+
},
68+
},
69+
}
70+
</script>
71+
72+
<style lang="scss" scoped>
73+
@import 'src/scss/webmapviewer-bootstrap-theme';
74+
75+
.map-popover {
76+
pointer-events: none;
77+
.card {
78+
max-width: $overlay-width;
79+
pointer-events: auto;
80+
}
81+
.map-popover-content {
82+
max-height: 350px;
83+
overflow-y: auto;
84+
}
85+
.card-body {
86+
display: flex;
87+
flex-direction: column;
88+
}
89+
// Triangle border
90+
$arrow-height: 12px;
91+
&::before {
92+
position: absolute;
93+
top: -($arrow-height * 2);
94+
left: 50%;
95+
margin-left: -$arrow-height;
96+
border: $arrow-height solid transparent;
97+
border-bottom-color: $border-color-translucent;
98+
content: '';
99+
}
100+
// Triangle background
101+
&::after {
102+
$arrow-border-height: $arrow-height - 1;
103+
content: '';
104+
border: $arrow-border-height solid transparent;
105+
border-bottom-color: $light;
106+
position: absolute;
107+
top: -($arrow-border-height * 2);
108+
left: 50%;
109+
margin-left: -$arrow-border-height;
110+
}
111+
}
112+
@media (min-height: 600px) {
113+
.map-popover .card-body {
114+
max-height: 510px;
115+
}
116+
}
117+
</style>

src/modules/map/components/cesium/CesiumMap.vue

+188-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
:z-index="index + startingZIndexForVisibleLayers"
1818
/>
1919
<CesiumInternalLayer
20-
v-for="(layer, index) in visiblePrimitiveLayers"
20+
v-for="(layer, index) in visibleGeoJsonLayers"
2121
:key="layer.getID()"
2222
:layer-config="layer"
2323
:preview-year="previewYear"
@@ -31,6 +31,25 @@
3131
:z-index="index"
3232
/>
3333
</div>
34+
<CesiumPopover
35+
v-if="showFeaturesPopover"
36+
:coordinates="popoverCoordinates"
37+
authorize-print
38+
@close="onPopupClose"
39+
:use-content-padding="!!editFeature"
40+
>
41+
<template #extra-buttons>
42+
<button
43+
class="btn btn-sm btn-light d-flex align-items-center"
44+
data-cy="toggle-floating-off"
45+
@click="toggleFloatingTooltip"
46+
>
47+
<FontAwesomeIcon icon="caret-down" />
48+
</button>
49+
</template>
50+
<FeatureEdit v-if="editFeature" :read-only="true" :feature="editFeature" />
51+
<FeatureList direction="column" />
52+
</CesiumPopover>
3453
</div>
3554
<cesium-compass v-show="isDesktopMode" ref="compass"></cesium-compass>
3655
<slot />
@@ -51,10 +70,13 @@ import '@geoblocks/cesium-compass'
5170
import * as cesium from 'cesium'
5271
import {
5372
Cartesian3,
73+
Cartographic,
5474
CesiumTerrainProvider,
5575
Color,
5676
Math as CesiumMath,
5777
RequestScheduler,
78+
ScreenSpaceEventHandler,
79+
ScreenSpaceEventType,
5880
Viewer,
5981
} from 'cesium'
6082
import { mapActions, mapGetters, mapState } from 'vuex'
@@ -70,9 +92,29 @@ import { calculateHeight, limitCameraCenter, limitCameraPitchRoll } from './util
7092
import GeoAdminWMSLayer from '@/api/layers/GeoAdminWMSLayer.class'
7193
import GeoAdminGeoJsonLayer from '@/api/layers/GeoAdminGeoJsonLayer.class'
7294
import KMLLayer from '@/api/layers/KMLLayer.class'
95+
import CesiumPopover from '@/modules/map/components/cesium/CesiumPopover.vue'
96+
import { ClickInfo, ClickType } from '@/store/modules/map.store'
97+
import proj4 from 'proj4'
98+
import { LV95, WEBMERCATOR, WGS84 } from '@/utils/coordinateSystems'
99+
import FeatureList from '@/modules/infobox/components/FeatureList.vue'
100+
import {
101+
highlightGroup,
102+
unhighlightGroup,
103+
} from '@/modules/map/components/cesium/utils/highlightUtils'
104+
import { createGeoJSONFeature } from '@/utils/layerUtils'
105+
import {
106+
isInBounds,
107+
LV95_BOUNDS,
108+
reprojectUnknownSrsCoordsToWebMercator,
109+
} from '@/utils/coordinateUtils'
110+
import { extractOlFeatureGeodesicCoordinates } from '@/modules/drawing/lib/drawingUtils'
111+
import log from '@/utils/logging'
112+
import { LineString, Point, Polygon } from 'ol/geom'
113+
import FeatureEdit from '@/modules/infobox/components/FeatureEdit.vue'
114+
import OpenLayersPopover from '@/modules/map/components/openlayers/OpenLayersPopover.vue'
73115
74116
export default {
75-
components: { CesiumInternalLayer },
117+
components: { OpenLayersPopover, FeatureEdit, FeatureList, CesiumPopover, CesiumInternalLayer },
76118
provide() {
77119
return {
78120
// sharing cesium viewer object with children components
@@ -97,6 +139,7 @@ export default {
97139
false,
98140
[]
99141
),
142+
popoverCoordinates: [],
100143
}
101144
},
102145
computed: {
@@ -106,6 +149,8 @@ export default {
106149
camera: (state) => state.position.camera,
107150
uiMode: (state) => state.ui.mode,
108151
previewYear: (state) => state.layers.previewYear,
152+
isFeatureTooltipInFooter: (state) => !state.ui.floatingTooltip,
153+
selectedFeatures: (state) => state.features.selectedFeatures,
109154
}),
110155
...mapGetters(['centerEpsg4326', 'resolution', 'hasDevSiteWarning', 'visibleLayers']),
111156
isDesktopMode() {
@@ -119,12 +164,64 @@ export default {
119164
(l) => l instanceof GeoAdminWMTSLayer || l instanceof GeoAdminWMSLayer
120165
)
121166
},
122-
visiblePrimitiveLayers() {
167+
visibleGeoJsonLayers() {
123168
return this.visibleLayers.filter((l) => l instanceof GeoAdminGeoJsonLayer)
124169
},
125170
visibleKMLLayers() {
126171
return this.visibleLayers.filter((l) => l instanceof KMLLayer)
127172
},
173+
showFeaturesPopover() {
174+
return !this.isFeatureTooltipInFooter && this.selectedFeatures.length > 0
175+
},
176+
editFeature() {
177+
return this.selectedFeatures.find((feature) => feature.isEditable)
178+
},
179+
},
180+
watch: {
181+
selectedFeatures: {
182+
// we need to deep watch this as otherwise we aren't triggered when
183+
// coordinates are changed (but only when one feature is added/removed)
184+
handler(newSelectedFeatures) {
185+
if (newSelectedFeatures.length > 0) {
186+
const [firstFeature] = newSelectedFeatures
187+
const geometries = newSelectedFeatures.map((f) => {
188+
// GeoJSON and KML layers have different geometry structure
189+
if (!f.geometry.type) {
190+
let type = undefined
191+
if (f.geometry instanceof Polygon) {
192+
type = 'Polygon'
193+
} else if (f.geometry instanceof LineString) {
194+
type = 'LineString'
195+
} else if (f.geometry instanceof Point) {
196+
type = 'Point'
197+
}
198+
const coordinates = f.geometry.getCoordinates()
199+
const getCoordinates = (c) =>
200+
isInBounds(c[0], c[1], LV95_BOUNDS)
201+
? proj4(LV95.epsg, WEBMERCATOR.epsg, c)
202+
: c
203+
return {
204+
type,
205+
coordinates:
206+
typeof coordinates[0] === 'number'
207+
? getCoordinates(coordinates)
208+
: coordinates.map(getCoordinates),
209+
}
210+
}
211+
return f.geometry
212+
})
213+
highlightGroup(this.viewer, geometries)
214+
const featureCoords = Array.isArray(firstFeature.coordinates[0])
215+
? firstFeature.coordinates[firstFeature.coordinates.length - 1]
216+
: firstFeature.coordinates
217+
this.popoverCoordinates = reprojectUnknownSrsCoordsToWebMercator(
218+
featureCoords[0],
219+
featureCoords[1]
220+
)
221+
}
222+
},
223+
deep: true,
224+
},
128225
},
129226
beforeCreate() {
130227
// Global variable required for Cesium and point to the URL where four static directories (see vite.config) are served
@@ -203,6 +300,11 @@ export default {
203300
this.viewer.scene.postRender.addEventListener(
204301
limitCameraPitchRoll(CAMERA_MIN_PITCH, CAMERA_MAX_PITCH, 0.0, 0.0)
205302
)
303+
this.eventHandler = new ScreenSpaceEventHandler(this.viewer.canvas)
304+
this.eventHandler.setInputAction(
305+
(event) => this.onSingleClick(event),
306+
ScreenSpaceEventType.LEFT_CLICK
307+
)
206308
207309
this.flyToPosition()
208310
@@ -212,13 +314,21 @@ export default {
212314
globe.maximumScreenSpaceError = 30
213315
}
214316
},
317+
beforeUnmount() {
318+
this.clearAllSelectedFeatures()
319+
},
215320
unmounted() {
216321
this.setCameraPosition(null)
217322
this.viewer.destroy()
218323
delete this.viewer
219324
},
220325
methods: {
221-
...mapActions(['setCameraPosition']),
326+
...mapActions([
327+
'setCameraPosition',
328+
'clearAllSelectedFeatures',
329+
'click',
330+
'toggleFloatingTooltip',
331+
]),
222332
flyToPosition() {
223333
const x = this.camera ? this.camera.x : this.centerEpsg4326[0]
224334
const y = this.camera ? this.camera.y : this.centerEpsg4326[1]
@@ -252,6 +362,80 @@ export default {
252362
roll: CesiumMath.toDegrees(camera.roll).toFixed(0),
253363
})
254364
},
365+
onSingleClick(event) {
366+
this.clearAllSelectedFeatures()
367+
unhighlightGroup(this.viewer)
368+
const features = []
369+
let coordinates = []
370+
const cartesian = this.viewer.scene.pickPosition(event.position)
371+
if (cartesian) {
372+
const cartCoords = Cartographic.fromCartesian(cartesian)
373+
coordinates = proj4(WGS84.epsg, WEBMERCATOR.epsg, [
374+
(cartCoords.longitude * 180) / Math.PI,
375+
(cartCoords.latitude * 180) / Math.PI,
376+
])
377+
}
378+
379+
let objects = this.viewer.scene.drillPick(event.position)
380+
const geoJsonFeatures = {}
381+
const kmlFeatures = {}
382+
// if there is a GeoJSON layer currently visible, we will find it and search for features under the mouse cursor
383+
this.visibleGeoJsonLayers.forEach((geoJSonLayer) => {
384+
objects
385+
.filter((obj) => obj.primitive?.olLayer?.get('id') === geoJSonLayer.getID())
386+
.forEach((obj) => {
387+
const feature = obj.primitive.olFeature
388+
if (!geoJsonFeatures[feature.getId()]) {
389+
geoJsonFeatures[feature.getId()] = createGeoJSONFeature(
390+
obj.primitive.olFeature,
391+
geoJSonLayer,
392+
feature.getGeometry()
393+
)
394+
}
395+
})
396+
features.push(...Object.values(geoJsonFeatures))
397+
})
398+
this.visibleKMLLayers.forEach((KMLLayer) => {
399+
objects
400+
.filter((obj) => obj.primitive?.olLayer?.get('id') === KMLLayer.getID())
401+
.forEach((obj) => {
402+
const feature = obj.primitive.olFeature
403+
if (!kmlFeatures[feature.getId()]) {
404+
const editableFeature = feature.get('editableFeature')
405+
if (editableFeature) {
406+
editableFeature.geodesicCoordinates =
407+
extractOlFeatureGeodesicCoordinates(feature)
408+
editableFeature.geometry = feature.getGeometry()
409+
kmlFeatures[feature.getId()] = editableFeature
410+
} else {
411+
log.debug(
412+
'KMLs which are not editable Features are not supported for selection'
413+
)
414+
}
415+
}
416+
})
417+
features.push(...Object.values(kmlFeatures))
418+
})
419+
// Cesium can't pick position when click on primitive
420+
if (!coordinates.length && features.length) {
421+
const featureCoords = Array.isArray(features[0].coordinates[0])
422+
? features[0].coordinates[0]
423+
: features[0].coordinates
424+
coordinates = proj4(LV95.epsg, WEBMERCATOR.epsg, featureCoords)
425+
}
426+
this.click(
427+
new ClickInfo(
428+
coordinates,
429+
[event.position.x, event.position.y],
430+
features,
431+
ClickType.LEFT_SINGLECLICK
432+
)
433+
)
434+
},
435+
onPopupClose() {
436+
this.clearAllSelectedFeatures()
437+
unhighlightGroup(this.viewer)
438+
},
255439
},
256440
}
257441
</script>

0 commit comments

Comments
 (0)