Skip to content

Commit 7a2cc91

Browse files
committed
BGDIINF_SB-2980: Add GeoJSON layer component for 3d
BGDIINF_SB-2980: Add GeoJSON layer component for 3d
1 parent e9fc97f commit 7a2cc91

8 files changed

+256
-37
lines changed

package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
"test:e2e:ci:prod": "npm run test:e2e:ci",
1919
"test:ci": "npm run test:unit && npm run test:e2e:ci",
2020
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
21-
"format": "prettier --write src",
22-
"format-lint": "npm run format && npm run lint",
21+
"format": "prettier --write src",
22+
"format-lint": "npm run format && npm run lint",
2323
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
2424
"build": "npm run type-check && vite build",
2525
"build:dev": "npm run build -- --mode development",
@@ -55,6 +55,7 @@
5555
"liang-barsky": "^1.0.5",
5656
"maplibre-gl": "^3.1.0",
5757
"ol": "^7.4.0",
58+
"ol-cesium": "^2.14.0",
5859
"pako": "^2.1.0",
5960
"print-js": "^1.6.0",
6061
"proj4": "^2.9.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<template>
2+
<div>
3+
<slot />
4+
</div>
5+
</template>
6+
7+
<script>
8+
import { PrimitiveCollection } from 'cesium'
9+
import axios from 'axios'
10+
import OlStyleForPropertyValue from '@/modules/map/components/openlayers/utils/styleFromLiterals'
11+
import FeatureConverter from 'ol-cesium/src/olcs/FeatureConverter'
12+
import GeoJSON from 'ol/format/GeoJSON'
13+
import { Vector as VectorLayer } from 'ol/layer'
14+
import { Vector as VectorSource } from 'ol/source'
15+
import { updateCollectionOpacity } from '@/modules/map/components/cesium/utils/geoJsonUtils'
16+
import addLayerToViewer from '@/modules/map/components/cesium/utils/addLayerToViewer-mixins'
17+
import log from '@/utils/logging'
18+
19+
const STYLE_RESOLUTION = 100
20+
21+
/** Adds a GeoJSON layer to the Cesium viewer */
22+
export default {
23+
mixins: [addLayerToViewer],
24+
props: {
25+
layerId: {
26+
type: String,
27+
required: true,
28+
},
29+
geojsonUrl: {
30+
type: String,
31+
required: true,
32+
},
33+
styleUrl: {
34+
type: String,
35+
required: true,
36+
},
37+
opacity: {
38+
type: Number,
39+
default: 0.9,
40+
},
41+
},
42+
methods: {
43+
addLayer(layer) {
44+
this.getViewer().scene.primitives.add(layer)
45+
this.isPresentOnMap = true
46+
},
47+
removeLayer(layer) {
48+
const viewer = this.getViewer()
49+
layer.removeAll()
50+
viewer.scene.primitives.remove(layer)
51+
viewer.scene.requestRender()
52+
this.isPresentOnMap = false
53+
},
54+
},
55+
watch: {
56+
opacity(newOpacity) {
57+
updateCollectionOpacity(this.layer, newOpacity)
58+
this.getViewer().scene.requestRender()
59+
},
60+
},
61+
created() {
62+
const scene = this.getViewer().scene
63+
this.layer = new PrimitiveCollection()
64+
const featureConverter = new FeatureConverter(scene)
65+
const olLayer = new VectorLayer({
66+
id: this.layerId,
67+
opacity: this.opacity,
68+
properties: { altitudeMode: 'relativeToGround' },
69+
})
70+
Promise.all([axios.get(this.geojsonUrl), axios.get(this.styleUrl)])
71+
.then((responses) => {
72+
const geojsonData = responses[0].data
73+
const geojsonStyleLiterals = responses[1].data
74+
const style = new OlStyleForPropertyValue(geojsonStyleLiterals)
75+
olLayer.setSource(
76+
new VectorSource({
77+
features: new GeoJSON().readFeatures(geojsonData),
78+
})
79+
)
80+
olLayer.setStyle(function (feature, res) {
81+
return style.getFeatureStyle(feature, res)
82+
})
83+
const counterpart = featureConverter.olVectorLayerToCesium(
84+
olLayer,
85+
{
86+
getProjection: () =>
87+
geojsonData.crs ? geojsonData.crs.properties.name : null,
88+
getResolution: () => STYLE_RESOLUTION,
89+
},
90+
{}
91+
)
92+
// need to wait for terrain loaded otherwise primitives will be placed wrong
93+
if (this.layer) {
94+
if (scene.globe.tilesLoaded) {
95+
this.layer.add(counterpart.getRootPrimitive())
96+
updateCollectionOpacity(this.layer, this.opacity)
97+
this.getViewer().scene.requestRender()
98+
} else {
99+
const unlisten = scene.globe.tileLoadProgressEvent.addEventListener(
100+
(queueLength) => {
101+
if (scene.globe.tilesLoaded && queueLength === 0) {
102+
this.layer.add(counterpart.getRootPrimitive())
103+
updateCollectionOpacity(this.layer, this.opacity)
104+
this.getViewer().scene.requestRender()
105+
unlisten()
106+
}
107+
}
108+
)
109+
}
110+
}
111+
})
112+
.catch((error) => {
113+
log.error(
114+
'Error while fetching GeoJSON data/style for layer ' + this.layerId,
115+
error
116+
)
117+
})
118+
},
119+
}
120+
</script>

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

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
:projection="LV95"
1515
:z-index="zIndex"
1616
/>
17+
<CesiumGeoJSONLayer
18+
v-if="layerConfig.type === LayerTypes.GEOJSON"
19+
:layer-id="layerConfig.getID()"
20+
:opacity="layerConfig.opacity"
21+
:geojson-url="layerConfig.geoJsonUrl"
22+
:style-url="layerConfig.styleUrl"
23+
/>
1724
<slot />
1825
</div>
1926
</template>
@@ -24,13 +31,15 @@ import LayerTypes from '@/api/layers/LayerTypes.enum'
2431
import { LV95, WEBMERCATOR } from '@/utils/coordinateSystems'
2532
import CesiumWMTSLayer from './CesiumWMTSLayer.vue'
2633
import CesiumWMSLayer from './CesiumWMSLayer.vue'
34+
import CesiumGeoJSONLayer from './CesiumGeoJSONLayer.vue'
2735
2836
/**
2937
* Transforms a layer config (metadata, structures can be found in api/layers/** files) into the
3038
* correct Cesium counterpart depending on the layer type.
3139
*/
3240
export default {
3341
components: {
42+
CesiumGeoJSONLayer,
3443
CesiumWMTSLayer,
3544
CesiumWMSLayer,
3645
},

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

+33-5
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88
:z-index="0"
99
/>
1010
<!-- Adding all other layers -->
11+
<!-- Layers split for correct zIndex ordering -->
1112
<CesiumInternalLayer
12-
v-for="(layer, index) in visibleLayers"
13+
v-for="(layer, index) in visibleImageryLayers"
1314
:key="layer.getID()"
1415
:layer-config="layer"
1516
:preview-year="previewYear"
1617
:z-index="index + startingZIndexForVisibleLayers"
1718
/>
19+
<CesiumInternalLayer
20+
v-for="(layer, index) in visiblePrimitiveLayers"
21+
:key="layer.getID()"
22+
:layer-config="layer"
23+
:preview-year="previewYear"
24+
:z-index="index"
25+
/>
1826
</div>
1927
</div>
2028
<cesium-compass v-show="isDesktopMode" ref="compass"></cesium-compass>
@@ -45,6 +53,8 @@ import GeoAdminWMTSLayer from '@/api/layers/GeoAdminWMTSLayer.class'
4553
import LayerTimeConfig from '@/api/layers/LayerTimeConfig.class'
4654
import { CURRENT_YEAR_WMTS_TIMESTAMP } from '@/api/layers/LayerTimeConfigEntry.class'
4755
import '@geoblocks/cesium-compass'
56+
import GeoAdminWMSLayer from '@/api/layers/GeoAdminWMSLayer.class'
57+
import GeoAdminGeoJsonLayer from '@/api/layers/GeoAdminGeoJsonLayer.class'
4858
4959
export default {
5060
components: { CesiumInternalLayer },
@@ -89,12 +99,23 @@ export default {
8999
startingZIndexForVisibleLayers() {
90100
return this.currentBackgroundLayer ? 1 : 0
91101
},
102+
visibleImageryLayers() {
103+
return this.visibleLayers.filter(
104+
(l) => l instanceof GeoAdminWMTSLayer || l instanceof GeoAdminWMSLayer
105+
)
106+
},
107+
visiblePrimitiveLayers() {
108+
return this.visibleLayers.filter((l) => l instanceof GeoAdminGeoJsonLayer)
109+
},
92110
},
93111
beforeCreate() {
94112
// Global variable required for Cesium and point to the URL where four static directories (see vite.config) are served
95113
// https://cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/#install-with-npm
96114
window['CESIUM_BASE_URL'] = '.'
97115
116+
// required for ol-cesium
117+
window['Cesium'] = cesium
118+
98119
// A per server key list of overrides to use for throttling limits.
99120
// Useful when streaming data from a known HTTP/2 or HTTP/3 server.
100121
Object.assign(RequestScheduler.requestsByServer, {
@@ -165,7 +186,9 @@ export default {
165186
this.viewerCreated = true
166187
167188
this.viewer.scene.postRender.addEventListener(limitCameraCenter(TILEGRID_EXTENT_EPSG_4326))
168-
this.viewer.scene.postRender.addEventListener(limitCameraPitchRoll(CAMERA_MIN_PITCH, CAMERA_MAX_PITCH, 0.0, 0.0))
189+
this.viewer.scene.postRender.addEventListener(
190+
limitCameraPitchRoll(CAMERA_MIN_PITCH, CAMERA_MAX_PITCH, 0.0, 0.0)
191+
)
169192
170193
this.flyToPosition()
171194
},
@@ -179,9 +202,13 @@ export default {
179202
flyToPosition() {
180203
const x = this.camera ? this.camera.x : this.centerEpsg4326[0]
181204
const y = this.camera ? this.camera.y : this.centerEpsg4326[1]
182-
const z = this.camera ? this.camera.z : calculateHeight(this.resolution, this.viewer.canvas.clientWidth)
205+
const z = this.camera
206+
? this.camera.z
207+
: calculateHeight(this.resolution, this.viewer.canvas.clientWidth)
183208
const heading = this.camera ? CesiumMath.toRadians(this.camera.heading) : -this.rotation
184-
const pitch = this.camera ? CesiumMath.toRadians(this.camera.pitch) : -CesiumMath.PI_OVER_TWO
209+
const pitch = this.camera
210+
? CesiumMath.toRadians(this.camera.pitch)
211+
: -CesiumMath.PI_OVER_TWO
185212
const roll = this.camera ? CesiumMath.toRadians(this.camera.roll) : 0
186213
this.viewer.camera.flyTo({
187214
destination: Cartesian3.fromDegrees(x, y, z),
@@ -219,7 +246,8 @@ export default {
219246
height: 100%;
220247
z-index: $zindex-map;
221248

222-
.cesium-viewer, .cesium-widget canvas {
249+
.cesium-viewer,
250+
.cesium-widget canvas {
223251
position: absolute;
224252
width: 100%;
225253
height: 100%;

src/modules/map/components/cesium/utils/addImageryLayer-mixins.js

+9-30
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import addLayerToViewer from '@/modules/map/components/cesium/utils/addLayerToViewer-mixins'
2+
13
/**
24
* Vue mixin that will handle the addition or removal of a Cesium Imagery layer. This is a
35
* centralized way of describing this logic.
@@ -16,26 +18,9 @@
1618
* imagery layers in Cesium viewer.
1719
*/
1820
const addImageryLayerMixins = {
19-
inject: ['getViewer'],
20-
data() {
21-
return {
22-
isPresentOnMap: false,
23-
}
24-
},
25-
mounted() {
26-
if (this.layer && !this.isPresentOnMap) {
27-
this.addLayer(this.zIndex, this.layer)
28-
}
29-
},
30-
unmounted() {
31-
if (this.layer && this.isPresentOnMap) {
32-
this.removeLayer(this.layer)
33-
}
34-
35-
delete this.layer
36-
},
21+
mixins: [addLayerToViewer],
3722
methods: {
38-
addLayer(zIndex, layer) {
23+
addLayer(layer, zIndex) {
3924
const viewer = this.getViewer()
4025
viewer.scene.imageryLayers.add(layer, zIndex)
4126
this.isPresentOnMap = true
@@ -48,7 +33,7 @@ const addImageryLayerMixins = {
4833
watch: {
4934
opacity(newOpacity) {
5035
this.layer.alpha = newOpacity
51-
this.getViewer().scene.render()
36+
this.getViewer().scene.requestRender()
5237
},
5338
url(newUrl) {
5439
const viewer = this.getViewer()
@@ -57,18 +42,12 @@ const addImageryLayerMixins = {
5742
this.layer = this.createImagery(newUrl)
5843
viewer.scene.imageryLayers.add(this.layer, index)
5944
},
60-
zIndex(zIndex) {
45+
zIndex() {
6146
if (this.layer) {
6247
const imageryLayers = this.getViewer().scene.imageryLayers
63-
const index = imageryLayers.indexOf(this.layer)
64-
const indexDiff = Math.abs(zIndex - index)
65-
for (let i = indexDiff; i !== 0; i--) {
66-
if (index > zIndex) {
67-
imageryLayers.lower(this.layer)
68-
} else {
69-
imageryLayers.raise(this.layer)
70-
}
71-
}
48+
imageryLayers.lowerToBottom(this.layer)
49+
// raise one time to place layer above base layer
50+
imageryLayers.raise(this.layer)
7251
}
7352
},
7453
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Vue mixin that will handle the addition or removal of a Cesium layer.
3+
*
4+
* Each component that uses this mixin must create a layer (`this.layer`) and next methods:
5+
*
6+
* - `addLayer` that gets `layer` and `zIndex` (optional) as properties
7+
* - `removeLayer` that gets `layer` as property
8+
*/
9+
const addLayerToViewer = {
10+
inject: ['getViewer'],
11+
data() {
12+
return {
13+
isPresentOnMap: false,
14+
}
15+
},
16+
mounted() {
17+
if (this.layer && !this.isPresentOnMap) {
18+
this.addLayer(this.layer, this.zIndex)
19+
}
20+
},
21+
unmounted() {
22+
if (this.layer && this.isPresentOnMap) {
23+
this.removeLayer(this.layer)
24+
}
25+
26+
delete this.layer
27+
},
28+
}
29+
30+
export default addLayerToViewer

0 commit comments

Comments
 (0)