@masterportal/masterportalapi 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,40 +1,60 @@
1
1
  import OLCesium from "olcs/OLCesium.js";
2
+ import {transform, get} from "ol/proj.js";
2
3
  import VectorSynchronizer from "olcs/VectorSynchronizer.js";
4
+
3
5
  import FixedOverlaySynchronizer from "./3dUtils/fixedOverlaySynchronizer.js";
4
6
  import WMSRasterSynchronizer from "./3dUtils/wmsRasterSynchronizer.js";
5
- import {transform, get} from "ol/proj.js";
7
+ import defaults from "../../defaults";
6
8
 
7
9
  let mapIdCounter = 0;
8
10
 
9
11
  /**
10
- * Sets the cesium scene defaults from the config.js
11
- * @param {Cesium.scene} scene Cesium scene.
12
- * @param {Object} config optional configuration parameters.
12
+ * Sets the cesium default scene params.
13
+ * @param {Cesium.scene} scene The cesium scene.
14
+ * @param {Object} params Optional configuration parameters.
13
15
  * @returns {void}
14
16
  */
15
- function setCesiumSceneDefaults (scene, config) {
16
- let params;
17
-
18
- if (Object.prototype.hasOwnProperty.call(config, "cesiumParameter")) {
19
- params = config.cesiumParameter;
20
- if (params?.fog) {
21
- scene.fog.enabled = params.fog?.enabled ? params.fog.enabled : scene.fog.enabled;
22
- scene.fog.density = params.fog?.density ? parseFloat(params.fog.density) : scene.fog.density;
23
- scene.fog.screenSpaceErrorFactor = params.fog?.screenSpaceErrorFactor ? parseFloat(params.fog.screenSpaceErrorFactor) : scene.fog.screenSpaceErrorFactor;
17
+ export function setCesiumSceneParams (scene, params) {
18
+ Object.keys(params).forEach(paramKey => {
19
+ const paramValue = params[paramKey];
20
+
21
+ if (typeof paramValue === "object") {
22
+ Object.keys(paramValue).forEach(paramSubKey => {
23
+ scene[paramKey][paramSubKey] = paramValue[paramSubKey];
24
+ });
25
+ }
26
+ else {
27
+ scene[paramKey] = paramValue;
28
+ }
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Creates a 3D-map.
34
+ * @param {object} [settings] The settings for the 3D-map.
35
+ * @param {module:ol/PluggableMap~PluggableMap} [settings.map2D] The 2D-Map
36
+ * @param {Cesium.JulianDate} [settings.shadowTime] The shadow time in julian date format if undefined olcs default is Cesium.JulianDate.now().
37
+ * @returns {module:OLCesium} the 3d-map
38
+ */
39
+ export function createMap (settings) {
40
+ const map3D = new OLCesium({
41
+ map: settings.map2D,
42
+ time: settings?.shadowTime,
43
+ stopOpenLayersEventsPropagation: true,
44
+ createSynchronizers: (olMap, scene) => {
45
+ return [new WMSRasterSynchronizer(olMap, scene), new VectorSynchronizer(olMap, scene), new FixedOverlaySynchronizer(olMap, scene)];
24
46
  }
47
+ });
25
48
 
26
- scene.globe.tileCacheSize = params?.tileCacheSize ? parseInt(params.tileCacheSize, 10) : scene.globe.tileCacheSize;
27
- scene.globe.maximumScreenSpaceError = params?.maximumScreenSpaceError ? params.maximumScreenSpaceError : scene.globe.maximumScreenSpaceError;
28
- scene.shadowMap.maximumDistance = 5000.0;
29
- scene.shadowMap.darkness = 0.6;
30
- scene.shadowMap.size = 2048;
31
- scene.fxaa = params?.fxaa ? params.fxaa : scene.fxaa;
32
- scene.globe.enableLighting = params?.enableLighting ? params.enableLighting : scene.globe.enableLighting;
33
- scene.globe.depthTestAgainstTerrain = true;
34
- scene.highDynamicRange = false;
35
- scene.pickTranslucentDepth = true;
36
- scene.camera.enableTerrainAdjustmentWhenLoading = true;
49
+ map3D.id = `map3D_${mapIdCounter++}`;
50
+ map3D.mapMode = "3D";
51
+
52
+ setCesiumSceneParams(map3D.getCesiumScene(), defaults.sceneOptions);
53
+ if (settings?.cesiumParameter) {
54
+ setCesiumSceneParams(map3D.getCesiumScene(), settings.cesiumParameter);
37
55
  }
56
+
57
+ return map3D;
38
58
  }
39
59
 
40
60
  /**
@@ -104,22 +124,29 @@ function reactTo3DClickEvent (event) {
104
124
  document.querySelector(".nav li").classList.remove("open");
105
125
  }
106
126
  cartographic = scene.globe.ellipsoid.cartesianToCartographic(cartesian);
107
- coords = [window.Cesium.Math.toDegrees(cartographic.longitude), window.Cesium.Math.toDegrees(cartographic.latitude)];
127
+ coords = [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)];
108
128
  height = scene.globe.getHeight(cartographic);
109
129
  if (height) {
110
130
  coords = coords.concat([height]);
111
131
  }
112
132
 
113
- distance = window.Cesium.Cartesian3.distance(cartesian, scene.camera.position);
133
+ distance = Cesium.Cartesian3.distance(cartesian, scene.camera.position);
114
134
  resolution = this.map3D.getCamera().calcResolutionForDistance(distance, cartographic.latitude);
115
135
  transformedCoords = transform(coords, get("EPSG:4326"), mapProjection);
116
136
  transformedPickedPosition = null;
117
137
 
118
138
  if (scene.pickPositionSupported) {
139
+ const pickedObject = scene.pick(event.position);
140
+
119
141
  pickedPositionCartesian = scene.pickPosition(event.position);
142
+
143
+ if (!pickedPositionCartesian && pickedObject?.primitive instanceof window.Cesium.Billboard) {
144
+ pickedPositionCartesian = pickedObject.primitive?.position;
145
+ }
146
+
120
147
  if (pickedPositionCartesian) {
121
148
  cartographicPickedPosition = scene.globe.ellipsoid.cartesianToCartographic(pickedPositionCartesian);
122
- transformedPickedPosition = transform([window.Cesium.Math.toDegrees(cartographicPickedPosition.longitude), window.Cesium.Math.toDegrees(cartographicPickedPosition.latitude)], get("EPSG:4326"), mapProjection);
149
+ transformedPickedPosition = transform([Cesium.Math.toDegrees(cartographicPickedPosition.longitude), Cesium.Math.toDegrees(cartographicPickedPosition.latitude)], get("EPSG:4326"), mapProjection);
123
150
  transformedPickedPosition.push(cartographicPickedPosition.height);
124
151
  }
125
152
  }
@@ -142,51 +169,23 @@ function reactTo3DClickEvent (event) {
142
169
  * @param {Object} map3DObject Contains the scene, 3D map and a callback function.
143
170
  * @returns {void}
144
171
  */
145
- function handle3DEvents (map3DObject) {
172
+ export function handle3DEvents (map3DObject) {
146
173
  let eventHandler;
147
174
 
148
- if (window.Cesium) {
149
- eventHandler = new window.Cesium.ScreenSpaceEventHandler(map3DObject.scene.canvas);
150
- eventHandler.setInputAction(reactTo3DClickEvent.bind(map3DObject), window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
175
+ if (Cesium) {
176
+ eventHandler = new Cesium.ScreenSpaceEventHandler(map3DObject.scene.canvas);
177
+ eventHandler.setInputAction(reactTo3DClickEvent.bind(map3DObject), Cesium.ScreenSpaceEventType.LEFT_CLICK);
151
178
  }
152
179
  }
153
180
 
154
- /**
155
- * Creates a 3D-map.
156
- * @param {object} [settings] The settings for the 3D-map.
157
- * @param {module:ol/PluggableMap~PluggableMap} [settings.map2D] The 2D-Map
158
- * @param {Cesium.JulianDate} [settings.shadowTime] The shadow time in julian date format if undefined olcs default is Cesium.JulianDate.now().
159
- * @returns {module:OLCesium} the 3d-map
160
- */
161
- export function createMap (settings) {
162
- const map3D = new OLCesium({
163
- map: settings.map2D,
164
- time: settings.shadowTime ? settings.shadowTime : undefined,
165
- sceneOptions: {
166
- shadows: false
167
- },
168
- stopOpenLayersEventsPropagation: true,
169
- createSynchronizers: (olMap, scene) => {
170
- return [new WMSRasterSynchronizer(olMap, scene), new VectorSynchronizer(olMap, scene), new FixedOverlaySynchronizer(olMap, scene)];
171
- }
172
- });
173
-
174
- map3D.id = `map3D_${mapIdCounter++}`;
175
- map3D.mapMode = "3D";
176
-
177
- return map3D;
178
- }
179
-
180
181
  /**
181
182
  * Prepares the camera and listens to camera changed events.
182
183
  * @param {Cesium.scene} scene Cesium scene.
183
184
  * @param {Object} urlParams optional urlParams for camera.
184
- * @param {Object} map3D olcs map.
185
185
  * @param {Object} config optional configuration parameters.
186
- * @param {Cesium} cesium optional cesium.
187
186
  * @returns {Cesium.scene.camera} Cesium camera.
188
187
  */
189
- export function prepareCamera (scene, urlParams, map3D, config, cesium) {
188
+ export function prepareCamera (scene, urlParams, config) {
190
189
  const camera = scene.camera;
191
190
  let cameraParameter = Object.prototype.hasOwnProperty.call(config, "cameraParameter") ? config.cameraParameter : {};
192
191
 
@@ -195,18 +194,8 @@ export function prepareCamera (scene, urlParams, map3D, config, cesium) {
195
194
  cameraParameter = urlParams?.tilt ? Object.assign(cameraParameter || {}, {tilt: urlParams?.tilt}) : cameraParameter;
196
195
 
197
196
  if (Object.keys(cameraParameter).length > 0) {
198
- setCameraParameter(cameraParameter, map3D, cesium);
197
+ setCesiumSceneParams(scene, {camera: cameraParameter});
199
198
  }
200
- return camera;
201
- }
202
199
 
203
- /**
204
- * Prepares the cesium scene.
205
- * @param {Object} map3DObject Contains the scene, 3D map and a callback function.
206
- * @param {Object} config optional configuration parameters.
207
- * @returns {void}
208
- */
209
- export function prepareScene (map3DObject, config) {
210
- handle3DEvents(map3DObject);
211
- setCesiumSceneDefaults(map3DObject.scene, config);
200
+ return camera;
212
201
  }
@@ -0,0 +1,277 @@
1
+ import * as entities from "../../src/layer/entities";
2
+
3
+ describe("entities.js", function () {
4
+ let attr,
5
+ map,
6
+ dataSources = [],
7
+ dataSourcesContent,
8
+ dataSourceValues,
9
+ cesiumCustomDataSourceSpy,
10
+ cesiumHeadingPitchRollSpy,
11
+ tick;
12
+
13
+ beforeEach(() => {
14
+ tick = () => {
15
+ return new Promise(resolve => {
16
+ setTimeout(resolve, 0);
17
+ });
18
+ };
19
+ dataSourcesContent = {};
20
+ dataSourceValues = [];
21
+ global.Cesium = {};
22
+ global.Cesium.Cartesian3 = {};
23
+ global.Cesium.Transforms = {};
24
+ global.Cesium.Cartesian3.fromDegrees = jest.fn();
25
+ global.Cesium.HeadingPitchRoll = jest.fn();
26
+ global.Cesium.Transforms.headingPitchRollQuaternion = jest.fn();
27
+ global.Cesium.CustomDataSource = function CustomDataSource (name) {
28
+ this.name = name;
29
+ this.entities = {
30
+ values: dataSourceValues,
31
+ add: (toAdd) => {
32
+ dataSourceValues.push(toAdd);
33
+ this.length++;
34
+ return toAdd;
35
+ },
36
+ contains: (value) => dataSourceValues.includes(value)
37
+ };
38
+ this.length = 0;
39
+ this.forEach = () => dataSourceValues.forEach;
40
+ };
41
+ attr = {
42
+ id: "4003",
43
+ name: "EntitiesLayer",
44
+ typ: "Entities3D",
45
+ entities: [
46
+ {
47
+ "url": "https://daten-hamburg.de/gdi3d/datasource-data/Simple_Building.glb",
48
+ "attributes": {
49
+ "name": "einfaches Haus in Planten und Blomen"
50
+ },
51
+ "latitude": 53.5631,
52
+ "longitude": 9.9800,
53
+ "height": 16,
54
+ "heading": 0,
55
+ "pitch": 0,
56
+ "roll": 0,
57
+ "scale": 5,
58
+ "allowPicking": true,
59
+ "show": true
60
+ }
61
+ ]
62
+ };
63
+ dataSources = {
64
+ getByName: (name) => {
65
+ return dataSourcesContent[name] ? [dataSourcesContent[name]] : [];
66
+ },
67
+ add: async (dataSource) => {
68
+ dataSourcesContent[dataSource.name] = dataSource;
69
+ return Promise.resolve(dataSource).then(function (value) {
70
+ return value;
71
+ });
72
+ }
73
+ };
74
+ map = {
75
+ getDataSources: () => {
76
+ return dataSources;
77
+ }
78
+ };
79
+ cesiumCustomDataSourceSpy = jest.spyOn(global.Cesium, "CustomDataSource");
80
+ cesiumHeadingPitchRollSpy = jest.spyOn(global.Cesium.Transforms, "headingPitchRollQuaternion");
81
+ });
82
+
83
+ afterEach(() => {
84
+ global.Cesium = null;
85
+ jest.clearAllMocks();
86
+ });
87
+
88
+ function checkAttributes (layer, attrs) {
89
+ expect(layer.get("name")).toEqual(attrs.name);
90
+ expect(layer.get("typ")).toEqual(attrs.typ);
91
+ expect(layer.get("id")).toEqual(attrs.id);
92
+ }
93
+
94
+ describe("createLayer", function () {
95
+ it("creates a wrapper for terrain layer without map", function () {
96
+ const layer = entities.createLayer(attr);
97
+
98
+ checkAttributes(layer, attr);
99
+ });
100
+ it("creates a wrapper for entities layer with map and show is false", async function () {
101
+ attr.entities[0].show = false;
102
+ const layer = entities.createLayer(attr, map);
103
+
104
+ await tick();
105
+
106
+ checkAttributes(layer, attr);
107
+ expect(dataSourceValues.length).toEqual(1);
108
+ expect(dataSourceValues[0].show).toEqual(false);
109
+ expect(Object.keys(dataSourcesContent).length).toEqual(1);
110
+ expect(cesiumCustomDataSourceSpy).toHaveBeenCalledTimes(1);
111
+ expect(cesiumHeadingPitchRollSpy).toHaveBeenCalledTimes(1);
112
+ });
113
+ it("creates a wrapper for terrain layer with map and show not set", async function () {
114
+ attr.entities[0].show = undefined;
115
+ const layer = entities.createLayer(attr, map);
116
+
117
+ await tick();
118
+
119
+ checkAttributes(layer, attr);
120
+ expect(dataSourceValues.length).toEqual(1);
121
+ expect(dataSourceValues[0].show).toEqual(false);
122
+ expect(Object.keys(dataSourcesContent).length).toEqual(1);
123
+ expect(cesiumCustomDataSourceSpy).toHaveBeenCalledTimes(1);
124
+ expect(cesiumHeadingPitchRollSpy).toHaveBeenCalledTimes(1);
125
+ });
126
+ it("creates a wrapper for terrain layer with map and show is true", async function () {
127
+ attr.entities[0].show = true;
128
+ const layer = entities.createLayer(attr, map);
129
+
130
+ await tick();
131
+
132
+ checkAttributes(layer, attr);
133
+ expect(dataSourceValues.length).toEqual(1);
134
+ expect(dataSourceValues[0].show).toEqual(true);
135
+ expect(Object.keys(dataSourcesContent).length).toEqual(1);
136
+ expect(cesiumCustomDataSourceSpy).toHaveBeenCalledTimes(1);
137
+ expect(cesiumHeadingPitchRollSpy).toHaveBeenCalledTimes(1);
138
+ });
139
+ });
140
+ describe("setVisible", function () {
141
+ let consoleWarnOrig;
142
+
143
+ beforeEach(() => {
144
+ consoleWarnOrig = console.warn;
145
+ console.warn = jest.fn();
146
+ });
147
+
148
+ afterEach(() => {
149
+ console.warn = consoleWarnOrig;
150
+ });
151
+
152
+ it("call setVisible without map shall not fail and shall do nothing", async function () {
153
+ attr.isSelected = false;
154
+ attr.entities[0].show = false;
155
+ const layer = entities.createLayer(attr, map);
156
+
157
+ entities.setVisible(true, attr);
158
+
159
+ await tick();
160
+
161
+ checkAttributes(layer, attr);
162
+ expect(dataSourceValues.length).toEqual(1);
163
+ expect(dataSourceValues[0].show).toEqual(false);
164
+ });
165
+ it("call setVisible without map and rawLayer shall not fail and shall do nothing", async function () {
166
+ attr.isSelected = false;
167
+ attr.entities[0].show = false;
168
+ const layer = entities.createLayer(attr, map);
169
+
170
+ entities.setVisible(true, null);
171
+
172
+ await tick();
173
+
174
+ checkAttributes(layer, attr);
175
+ expect(dataSourceValues.length).toEqual(1);
176
+ expect(dataSourceValues[0].show).toEqual(false);
177
+ });
178
+ it("call setVisible shall set show to true", async function () {
179
+ attr.isSelected = false;
180
+ attr.entities[0].show = false;
181
+ const layer = entities.createLayer(attr, map);
182
+
183
+ await tick();
184
+
185
+ expect(dataSourceValues[0].show).toEqual(false);
186
+
187
+ layer.setVisible(true, attr, map);
188
+
189
+ checkAttributes(layer, attr);
190
+ expect(dataSourceValues.length).toEqual(1);
191
+ expect(dataSourcesContent[attr.id].show).toEqual(true);
192
+ });
193
+ it("call setVisible shall only warn, if no dataSource found", async function () {
194
+ attr.isSelected = false;
195
+ attr.entities[0].show = false;
196
+ const layer = entities.createLayer(attr, map);
197
+
198
+ await tick();
199
+
200
+ checkAttributes(layer, attr);
201
+
202
+ attr.id = "notExist";
203
+ entities.setVisible(true, attr, map);
204
+
205
+ expect(dataSourceValues.length).toEqual(1);
206
+ expect(dataSourcesContent[attr.id]).toEqual(undefined);
207
+ expect(console.warn).toHaveBeenCalledTimes(1);
208
+ });
209
+ });
210
+ describe("get", function () {
211
+ it("calls get for attributes", async function () {
212
+ const layer = entities.createLayer(attr, map);
213
+
214
+ checkAttributes(layer, attr);
215
+ expect(layer.get(null)).toEqual(undefined);
216
+ expect(layer.get(undefined)).toEqual(undefined);
217
+ expect(layer.get("")).toEqual(undefined);
218
+ });
219
+ });
220
+ describe("addEntities", function () {
221
+ let consoleWarnOrig;
222
+
223
+ beforeEach(() => {
224
+ consoleWarnOrig = console.warn;
225
+ console.warn = jest.fn();
226
+ });
227
+
228
+ afterEach(() => {
229
+ console.warn = consoleWarnOrig;
230
+ });
231
+
232
+ it("addEntities without map shall not fail", async function () {
233
+ const layer = entities.createLayer(attr, map);
234
+
235
+ await tick();
236
+ layer.createDataSource(attr);
237
+ });
238
+ it("addEntities without rawLayer shall not fail", async function () {
239
+ const layer = entities.createLayer(attr, map);
240
+
241
+ await tick();
242
+ layer.createDataSource(null, map);
243
+ expect(console.warn).toHaveBeenCalledTimes(1);
244
+ });
245
+
246
+ it("addEntities shall add entity twice", async function () {
247
+ attr.entities[0].show = false;
248
+ const layer = entities.createLayer(attr, map);
249
+
250
+ await tick();
251
+
252
+ checkAttributes(layer, attr);
253
+ expect(dataSourceValues.length).toEqual(1);
254
+
255
+ layer.createDataSource(attr, map);
256
+ expect(dataSourceValues.length).toEqual(2);
257
+ expect(dataSourceValues[0].show).toEqual(false);
258
+ expect(dataSourceValues[1].show).toEqual(false);
259
+ });
260
+ it("addEntities shall add entity and set Visible", async function () {
261
+ attr.entities[0].show = true;
262
+ const layer = entities.createLayer(attr, map);
263
+
264
+ await tick();
265
+
266
+ checkAttributes(layer, attr);
267
+ expect(dataSourceValues.length).toEqual(1);
268
+ expect(dataSourceValues[0].show).toEqual(true);
269
+
270
+ layer.createDataSource(attr, map);
271
+ expect(dataSourceValues.length).toEqual(2);
272
+ expect(dataSourceValues[0].show).toEqual(true);
273
+ expect(dataSourceValues[1].show).toEqual(true);
274
+ });
275
+ });
276
+ });
277
+
@@ -36,25 +36,12 @@ describe("terrain.js", function () {
36
36
  }
37
37
 
38
38
  describe("createLayer", function () {
39
- it("creates a wrapper for terrain layer without map and not selected", function () {
40
- const layer = terrain.createLayer(attr);
41
-
42
- checkAttributes(layer, attr);
43
- });
44
- it("creates a wrapper for terrain layer with map and not selected", function () {
45
- attr.isSelected = false;
39
+ it("creates a wrapper for terrain layer with map", function () {
46
40
  const layer = terrain.createLayer(attr, map);
47
41
 
48
42
  checkAttributes(layer, attr);
49
43
  expect(cesiumTerrainProviderSpy).toHaveBeenCalledTimes(0);
50
44
  });
51
- it("creates a wrapper for terrain layer with map and selected", function () {
52
- attr.isSelected = true;
53
- const layer = terrain.createLayer(attr, map);
54
-
55
- checkAttributes(layer, attr);
56
- expect(cesiumTerrainProviderSpy).toHaveBeenCalledTimes(1);
57
- });
58
45
  });
59
46
  describe("setVisible", function () {
60
47
  it("calls setVisible without map shall not fail", function () {
@@ -45,22 +45,15 @@ describe("tileset.js", function () {
45
45
  }
46
46
 
47
47
  describe("createLayer", function () {
48
- it("creates tileset layer without map and not selected", function () {
48
+ it("creates tileset layer without map", function () {
49
49
  const layer = new Tileset(attr);
50
50
 
51
51
  checkAttributes(layer, attr);
52
52
  expect(layer.tileSet).toEqual(undefined);
53
+ expect(layer.tileset.show).toEqual(undefined);
53
54
  expect(cesium3DTilesetSpy).toHaveBeenCalledTimes(1);
54
55
  });
55
- it("creates tileset layer with map and selected", function () {
56
- attr.isSelected = true;
57
- const layer = new Tileset(attr, map);
58
-
59
- checkAttributes(layer, attr);
60
- expect(layer.tileset.show).toEqual(true);
61
- expect(cesium3DTilesetSpy).toHaveBeenCalledTimes(1);
62
- });
63
- it("creates 2 tileset layers with map and selected and checks attributes", function () {
56
+ it("creates 2 tileset layers with map and checks attributes", function () {
64
57
  const attr2 = {
65
58
  id: "4003",
66
59
  name: "name",
@@ -0,0 +1,26 @@
1
+ import {get as getProjection} from "ol/proj";
2
+ import {getWidth} from "ol/extent";
3
+
4
+ import * as wmts from "../../src/layer/wmts";
5
+
6
+ describe.only("wmts.js", function () {
7
+ describe("generateArrays", function () {
8
+ it("should fill the arrays resolutions and matrixIds with numbers", function () {
9
+ const size = getWidth(getProjection("EPSG:3857").getExtent()) / 256,
10
+ resLength = 20,
11
+ resolutions = new Array(resLength),
12
+ matrixIds = new Array(resLength);
13
+
14
+ wmts.generateArrays(resolutions, matrixIds, resLength, size);
15
+
16
+ resolutions.forEach((element, index) => {
17
+ expect(typeof element).toBe("number");
18
+ expect(resolutions[index]).toBe(size / Math.pow(2, index));
19
+ });
20
+ matrixIds.forEach((id, index) => {
21
+ expect(typeof id).toBe("number");
22
+ expect(matrixIds[index]).toBe(index);
23
+ });
24
+ });
25
+ });
26
+ });