@map-colonies/react-components 3.13.1 → 3.15.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +46 -11
  2. package/dist/assets/img/transparent-tile.png +0 -0
  3. package/dist/cesium-map/helpers/customImageryProviders.d.ts +34 -0
  4. package/dist/cesium-map/helpers/customImageryProviders.js +106 -0
  5. package/dist/cesium-map/helpers/utils.d.ts +17 -0
  6. package/dist/cesium-map/helpers/utils.js +142 -0
  7. package/dist/cesium-map/layers/wms.layer.js +7 -1
  8. package/dist/cesium-map/layers/wmts.layer.js +7 -1
  9. package/dist/cesium-map/layers/xyz.layer.js +7 -1
  10. package/dist/cesium-map/layers-manager.d.ts +4 -0
  11. package/dist/cesium-map/layers-manager.js +169 -12
  12. package/dist/cesium-map/map.d.ts +5 -0
  13. package/dist/cesium-map/map.js +40 -12
  14. package/dist/cesium-map/tools/scale-tracker.tool.js +4 -1
  15. package/dist/cesium-map/tools/zoom_level-tracker.tool.css +19 -0
  16. package/dist/cesium-map/tools/zoom_level-tracker.tool.d.ts +8 -0
  17. package/dist/cesium-map/tools/zoom_level-tracker.tool.js +131 -0
  18. package/package.json +2 -2
  19. package/public/assets/img/transparent-tile.png +0 -0
  20. package/src/lib/cesium-map/helpers/customImageryProviders.ts +173 -0
  21. package/src/lib/cesium-map/helpers/utils.ts +135 -0
  22. package/src/lib/cesium-map/layers/optimized-tile-requests.stories.tsx +279 -0
  23. package/src/lib/cesium-map/layers/wms.layer.tsx +10 -4
  24. package/src/lib/cesium-map/layers/wmts.layer.tsx +9 -4
  25. package/src/lib/cesium-map/layers/xyz.layer.tsx +9 -4
  26. package/src/lib/cesium-map/layers-manager.ts +223 -19
  27. package/src/lib/cesium-map/map.stories.tsx +1 -0
  28. package/src/lib/cesium-map/map.tsx +33 -0
  29. package/src/lib/cesium-map/settings/settings.stories.tsx +5 -1
  30. package/src/lib/cesium-map/tools/coordinates-tracker.tool.tsx +6 -1
  31. package/src/lib/cesium-map/tools/scale-tracker.tool.tsx +4 -1
  32. package/src/lib/cesium-map/tools/zoom_level-tracker.tool.css +19 -0
  33. package/src/lib/cesium-map/tools/zoom_level-tracker.tool.tsx +142 -0
  34. package/src/lib/date-range-picker/date-range-picker.spec.tsx +81 -81
  35. package/jest_html_reporters.html +0 -78
@@ -5,8 +5,10 @@ import {
5
5
  WebMapServiceImageryProvider,
6
6
  WebMapTileServiceImageryProvider,
7
7
  Event,
8
+ Rectangle,
9
+ SingleTileImageryProvider,
8
10
  } from 'cesium';
9
- import { get } from 'lodash';
11
+ import { get, isEmpty } from 'lodash';
10
12
  import { Feature, Point, Polygon } from 'geojson';
11
13
  import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
12
14
  import {
@@ -19,6 +21,13 @@ import { CesiumViewer } from './map';
19
21
  import { IBaseMap } from './settings/settings';
20
22
  import { pointToGeoJSON } from './tools/geojson/point.geojson';
21
23
  import { IMapLegend } from './map-legend';
24
+ import {
25
+ CustomUrlTemplateImageryProvider,
26
+ CustomWebMapServiceImageryProvider,
27
+ CustomWebMapTileServiceImageryProvider,
28
+ HAS_TRANSPARENCY_META_PROP,
29
+ } from './helpers/customImageryProviders';
30
+ import { cesiumRectangleContained } from './helpers/utils';
22
31
 
23
32
  const INC = 1;
24
33
  const DEC = -1;
@@ -52,6 +61,8 @@ export interface IVectorLayer {
52
61
 
53
62
  export type LegendExtractor = (layers: (any & { meta: any })[]) => IMapLegend[];
54
63
 
64
+ const TRANSPARENT_LAYER_ID = 'TRANSPARENT_BASE_LAYER';
65
+
55
66
  class LayerManager {
56
67
  public mapViewer: CesiumViewer;
57
68
 
@@ -74,10 +85,41 @@ class LayerManager {
74
85
  if (onLayersUpdate) {
75
86
  this.layerUpdated.addEventListener(onLayersUpdate, this);
76
87
  }
77
- this.mapViewer.imageryLayers.layerRemoved.addEventListener(() => {
78
- this.setLegends();
79
- this.layerUpdated.raiseEvent();
80
- });
88
+
89
+ // Binding layer's relevancy check to cesium lifecycle if optimized tile requests enabled.
90
+ if (this.mapViewer.shouldOptimizedTileRequests) {
91
+ this.layerUpdated.addEventListener((meta: Record<string, unknown>) => {
92
+ const newMetaKeys = Object.keys(meta);
93
+ const shouldTriggerRelevancyCheck =
94
+ newMetaKeys.length === 1 &&
95
+ newMetaKeys[0] === HAS_TRANSPARENCY_META_PROP;
96
+ if (shouldTriggerRelevancyCheck) {
97
+ this.markRelevantLayersForExtent();
98
+ this.hideNonRelevantLayers();
99
+ }
100
+ });
101
+
102
+ this.mapViewer.imageryLayers.layerRemoved.addEventListener(() => {
103
+ this.setLegends();
104
+ this.markRelevantLayersForExtent();
105
+ this.hideNonRelevantLayers();
106
+ });
107
+
108
+ this.mapViewer.imageryLayers.layerMoved.addEventListener(() => {
109
+ this.markRelevantLayersForExtent();
110
+ this.hideNonRelevantLayers();
111
+ });
112
+
113
+ this.mapViewer.imageryLayers.layerAdded.addEventListener(() => {
114
+ this.markRelevantLayersForExtent();
115
+ this.hideNonRelevantLayers();
116
+ });
117
+
118
+ this.mapViewer.camera.moveEnd.addEventListener(() => {
119
+ this.markRelevantLayersForExtent();
120
+ this.hideNonRelevantLayers();
121
+ });
122
+ }
81
123
  }
82
124
 
83
125
  /* eslint-disable */
@@ -87,9 +129,9 @@ class LayerManager {
87
129
  ): void {
88
130
  const layer = this.layers.find(layerPredicate);
89
131
  if (layer) {
90
- layer.meta = meta;
132
+ layer.meta = { ...(layer.meta ?? {}), ...meta };
91
133
  this.setLegends();
92
- this.layerUpdated.raiseEvent();
134
+ this.layerUpdated.raiseEvent(meta);
93
135
  }
94
136
  }
95
137
  /* eslint-enable */
@@ -101,6 +143,24 @@ class LayerManager {
101
143
  sortedBaseMapLayers.forEach((layer, idx) => {
102
144
  this.addRasterLayer(layer, idx, baseMap.id);
103
145
  });
146
+
147
+ /**
148
+ * Set transparent layer as the first layer. if using optimized tile requests.
149
+ *
150
+ * Apparently, cesium layer's rectangle is not affective when:
151
+ * - There is only one active layer && The layer's rectangle contains the extent rectangle.
152
+ *
153
+ * As a result, when using optimized tile requesting and we zoom in a discrete layer,
154
+ * there are some visual artifacts due to tiles requesting outside of the layer's rectangle boundary.
155
+ *
156
+ * A simple workaround would be adding a transparent layer as the very first layer at all times,
157
+ * so that we ensure the rectangle will always be affective.
158
+ */
159
+
160
+ if (this.mapViewer.shouldOptimizedTileRequests) {
161
+ this.removeLayer(TRANSPARENT_LAYER_ID);
162
+ this.addTransparentImageryProvider();
163
+ }
104
164
  }
105
165
 
106
166
  public addRasterLayer(
@@ -110,30 +170,47 @@ class LayerManager {
110
170
  ): void {
111
171
  let cesiumLayer: ICesiumImageryLayer | undefined;
112
172
  switch (layer.type) {
113
- case 'XYZ_LAYER':
173
+ case 'XYZ_LAYER': {
174
+ const options = layer.options as UrlTemplateImageryProvider.ConstructorOptions;
175
+
176
+ const providerInstance = this.mapViewer.shouldOptimizedTileRequests
177
+ ? new CustomUrlTemplateImageryProvider(options, this.mapViewer)
178
+ : new UrlTemplateImageryProvider(options);
179
+
114
180
  cesiumLayer = this.mapViewer.imageryLayers.addImageryProvider(
115
- new UrlTemplateImageryProvider(
116
- layer.options as UrlTemplateImageryProvider.ConstructorOptions
117
- ),
181
+ providerInstance,
118
182
  index
119
183
  );
184
+
120
185
  break;
121
- case 'WMS_LAYER':
186
+ }
187
+ case 'WMS_LAYER': {
188
+ const options = layer.options as WebMapServiceImageryProvider.ConstructorOptions;
189
+
190
+ const providerInstance = this.mapViewer.shouldOptimizedTileRequests
191
+ ? new CustomWebMapServiceImageryProvider(options, this.mapViewer)
192
+ : new WebMapServiceImageryProvider(options);
193
+
122
194
  cesiumLayer = this.mapViewer.imageryLayers.addImageryProvider(
123
- new WebMapServiceImageryProvider(
124
- layer.options as WebMapServiceImageryProvider.ConstructorOptions
125
- ),
195
+ providerInstance,
126
196
  index
127
197
  );
128
198
  break;
129
- case 'WMTS_LAYER':
199
+ }
200
+ case 'WMTS_LAYER': {
201
+ const options = layer.options as WebMapTileServiceImageryProvider.ConstructorOptions;
202
+
203
+ const providerInstance = this.mapViewer.shouldOptimizedTileRequests
204
+ ? new CustomWebMapTileServiceImageryProvider(options, this.mapViewer)
205
+ : new WebMapTileServiceImageryProvider(options);
206
+
130
207
  cesiumLayer = this.mapViewer.imageryLayers.addImageryProvider(
131
- new WebMapTileServiceImageryProvider(
132
- layer.options as WebMapTileServiceImageryProvider.ConstructorOptions
133
- ),
208
+ providerInstance,
134
209
  index
135
210
  );
211
+
136
212
  break;
213
+ }
137
214
  case 'OSM_LAYER':
138
215
  break;
139
216
  }
@@ -310,6 +387,29 @@ class LayerManager {
310
387
  });
311
388
  }
312
389
 
390
+ public addTransparentImageryProvider(): void {
391
+ // Worldwide transparent layer
392
+ const transparentLayer = this.mapViewer.imageryLayers.addImageryProvider(
393
+ new SingleTileImageryProvider({
394
+ url: './assets/img/transparent-tile.png',
395
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
396
+ rectangle: new Rectangle(
397
+ -3.141592653589793,
398
+ -1.5707963267948966,
399
+ 3.141592653589793,
400
+ 1.5707963267948966
401
+ ),
402
+ /* eslint-enable @typescript-eslint/no-magic-numbers */
403
+ }),
404
+ 0
405
+ );
406
+
407
+ (transparentLayer as ICesiumImageryLayer).meta = {
408
+ id: TRANSPARENT_LAYER_ID,
409
+ skipRelevancyCheck: true,
410
+ };
411
+ }
412
+
313
413
  private setLegends(): void {
314
414
  if (typeof this.legendsExtractor !== 'undefined') {
315
415
  this.legendsList = this.legendsExtractor(this.layers);
@@ -349,6 +449,110 @@ class LayerManager {
349
449
  }
350
450
  });
351
451
  }
452
+
453
+ private hideNonRelevantLayers(): void {
454
+ for (const layer of this.layers) {
455
+ if (
456
+ layer.meta?.relevantToExtent !== layer.show &&
457
+ layer.imageryProvider.ready
458
+ ) {
459
+ //@ts-ignore
460
+ layer.show = layer.meta?.relevantToExtent ?? true;
461
+ }
462
+ }
463
+ }
464
+
465
+ private markRelevantLayersForExtent(): void {
466
+ try {
467
+ const extent = this.mapViewer.camera.computeViewRectangle() as Rectangle;
468
+
469
+ // Iterating in reverse order so that top layer is first.
470
+ for (let i = this.layers.length - 1; i >= 0; i--) {
471
+ const layer = this.layers[i];
472
+ const intersectsExtent =
473
+ !isEmpty(extent) &&
474
+ !isEmpty(layer.rectangle) &&
475
+ Rectangle.intersection(extent, layer.rectangle);
476
+
477
+ // Iterating from top layer until the current. (inclusive)
478
+ for (let j = this.layers.length - 1; j >= i; j--) {
479
+ if (layer.meta?.skipRelevancyCheck === true) {
480
+ layer.meta = { ...layer.meta, relevantToExtent: true };
481
+ continue;
482
+ }
483
+
484
+ const layerAbove = this.layers[j];
485
+ const layerAboveHasTransparency =
486
+ layerAbove.meta?.[HAS_TRANSPARENCY_META_PROP] === true;
487
+
488
+ if (layer !== layerAbove) {
489
+ // Layer is relevant if in extent and there is no layer above it which is opaque and contains it.
490
+ if (intersectsExtent instanceof Rectangle) {
491
+ if (cesiumRectangleContained(extent, layer.rectangle)) {
492
+ // Layer contains the extent.
493
+ if (
494
+ cesiumRectangleContained(extent, layerAbove.rectangle) &&
495
+ !layerAboveHasTransparency
496
+ ) {
497
+ layer.meta = {
498
+ ...(layer.meta ?? {}),
499
+ relevantToExtent: false,
500
+ };
501
+ break;
502
+ } else {
503
+ layer.meta = {
504
+ ...(layer.meta ?? {}),
505
+ relevantToExtent: true,
506
+ };
507
+ }
508
+ }
509
+
510
+ if (
511
+ cesiumRectangleContained(extent, layerAbove.rectangle) &&
512
+ !layerAboveHasTransparency
513
+ ) {
514
+ layer.meta = { ...(layer.meta ?? {}), relevantToExtent: false };
515
+ break;
516
+ }
517
+
518
+ if (
519
+ cesiumRectangleContained(layer.rectangle, layerAbove.rectangle)
520
+ ) {
521
+ layer.meta = {
522
+ ...(layer.meta ?? {}),
523
+ relevantToExtent: layerAboveHasTransparency,
524
+ };
525
+
526
+ // Once there is layer above that hides it, no need to continue to check.
527
+ if (!layerAboveHasTransparency) {
528
+ break;
529
+ }
530
+ } else {
531
+ // Not contained by layer above it, and inside the extent.
532
+ layer.meta = { ...(layer.meta ?? {}), relevantToExtent: true };
533
+ }
534
+ } else {
535
+ layer.meta = { ...(layer.meta ?? {}), relevantToExtent: false };
536
+ }
537
+ } else {
538
+ // Handle top layer
539
+ if (i === this.layers.length - 1) {
540
+ layer.meta = {
541
+ ...(layer.meta ?? {}),
542
+ relevantToExtent: intersectsExtent instanceof Rectangle,
543
+ };
544
+ }
545
+ }
546
+ }
547
+ }
548
+ } catch (e) {
549
+ console.error(e);
550
+ }
551
+ }
552
+
553
+ public get layerList(): ICesiumImageryLayer[] {
554
+ return this.layers;
555
+ }
352
556
  }
353
557
 
354
558
  export default LayerManager;
@@ -121,6 +121,7 @@ LocalizedMap.argTypes = {
121
121
  MAP_SETTINGS_SCENE_MODE_TITLE: 'תצורה',
122
122
  MAP_SETTINGS_BASE_MAP_TITLE: 'מפות בסיס',
123
123
  MAP_SETTINGS_OK_BUTTON_TEXT: 'אישור',
124
+ ZOOM_LABEL: 'זום',
124
125
  },
125
126
  /* eslint-enable @typescript-eslint/naming-convention */
126
127
  },
@@ -27,6 +27,7 @@ import { Box } from '../box';
27
27
  import { Proj } from '../utils/projections';
28
28
  import { CoordinatesTrackerTool } from './tools/coordinates-tracker.tool';
29
29
  import { ScaleTrackerTool } from './tools/scale-tracker.tool';
30
+ import { ZoomLevelTrackerTool } from './tools/zoom_level-tracker.tool';
30
31
  import { CesiumSettings, IBaseMap, IBaseMaps } from './settings/settings';
31
32
  import { IMapLegend, MapLegendSidebar, MapLegendToggle } from './map-legend';
32
33
  import LayerManager, { LegendExtractor } from './layers-manager';
@@ -58,6 +59,7 @@ interface ICameraState {
58
59
  }
59
60
  export class CesiumViewer extends CesiumViewerCls {
60
61
  public layersManager?: LayerManager;
62
+ private useOptimizedTileRequests?: boolean;
61
63
 
62
64
  public constructor(
63
65
  container: string | Element,
@@ -65,6 +67,14 @@ export class CesiumViewer extends CesiumViewerCls {
65
67
  ) {
66
68
  super(container, options);
67
69
  }
70
+
71
+ public get shouldOptimizedTileRequests(): boolean {
72
+ return this.useOptimizedTileRequests ?? false;
73
+ }
74
+
75
+ public set shouldOptimizedTileRequests(useOptimizedTileRequests: boolean) {
76
+ this.useOptimizedTileRequests = useOptimizedTileRequests;
77
+ }
68
78
  }
69
79
 
70
80
  const mapContext = createContext<CesiumViewer | null>(null);
@@ -96,6 +106,7 @@ interface ILegends {
96
106
 
97
107
  export interface CesiumMapProps extends ViewerProps {
98
108
  showMousePosition?: boolean;
109
+ showZoomLevel?: boolean;
99
110
  showScale?: boolean;
100
111
  projection?: Proj;
101
112
  center?: [number, number];
@@ -103,6 +114,7 @@ export interface CesiumMapProps extends ViewerProps {
103
114
  locale?: { [key: string]: string };
104
115
  sceneModes?: CesiumSceneModeEnum[];
105
116
  baseMaps?: IBaseMaps;
117
+ useOptimizedTileRequests?: boolean;
106
118
  terrainProvider?: TerrainProvider;
107
119
  imageryContextMenu?: React.ReactElement<IContextMenuData>;
108
120
  imageryContextMenuSize?: {
@@ -128,6 +140,7 @@ export const CesiumMap: React.FC<CesiumMapProps> = (props) => {
128
140
  const [mapViewRef, setMapViewRef] = useState<CesiumViewer>();
129
141
  const [projection, setProjection] = useState<Proj>();
130
142
  const [showMousePosition, setShowMousePosition] = useState<boolean>();
143
+ const [showZoomLevel, setShowZoomLevel] = useState<boolean>();
131
144
  const [showScale, setShowScale] = useState<boolean>();
132
145
  const [locale, setLocale] = useState<{ [key: string]: string }>();
133
146
  const [cameraState, setCameraState] = useState<ICameraState | undefined>();
@@ -209,6 +222,9 @@ export const CesiumMap: React.FC<CesiumMapProps> = (props) => {
209
222
 
210
223
  useEffect(() => {
211
224
  if (mapViewRef) {
225
+ mapViewRef.shouldOptimizedTileRequests =
226
+ props.useOptimizedTileRequests ?? false;
227
+
212
228
  mapViewRef.layersManager = new LayerManager(
213
229
  mapViewRef,
214
230
  props.legends?.mapLegendsExtractor,
@@ -219,8 +235,16 @@ export const CesiumMap: React.FC<CesiumMapProps> = (props) => {
219
235
  }
220
236
  );
221
237
  }
238
+ // eslint-disable-next-line react-hooks/exhaustive-deps
222
239
  }, [mapViewRef]);
223
240
 
241
+ useEffect(() => {
242
+ if (mapViewRef) {
243
+ mapViewRef.shouldOptimizedTileRequests =
244
+ props.useOptimizedTileRequests ?? false;
245
+ }
246
+ }, [props.useOptimizedTileRequests, mapViewRef]);
247
+
224
248
  useEffect(() => {
225
249
  setSceneModes(
226
250
  props.sceneModes ?? [
@@ -254,6 +278,10 @@ export const CesiumMap: React.FC<CesiumMapProps> = (props) => {
254
278
  setShowMousePosition(props.showMousePosition ?? true);
255
279
  }, [props.showMousePosition]);
256
280
 
281
+ useEffect(() => {
282
+ setShowZoomLevel(props.showZoomLevel ?? true);
283
+ }, [props.showZoomLevel]);
284
+
257
285
  useEffect(() => {
258
286
  setShowScale(props.showScale ?? true);
259
287
  }, [props.showScale]);
@@ -391,6 +419,11 @@ export const CesiumMap: React.FC<CesiumMapProps> = (props) => {
391
419
  ) : (
392
420
  <></>
393
421
  )}
422
+ {showZoomLevel === true ? (
423
+ <ZoomLevelTrackerTool locale={locale} />
424
+ ) : (
425
+ <></>
426
+ )}
394
427
  {showScale === true ? <ScaleTrackerTool locale={locale} /> : <></>}
395
428
  </Box>
396
429
  </>,
@@ -171,7 +171,11 @@ export const MapWithSettings: Story = () => {
171
171
  center={center}
172
172
  zoom={14}
173
173
  imageryProvider={false}
174
- sceneModes={[CesiumSceneMode.SCENE3D, CesiumSceneMode.COLUMBUS_VIEW]}
174
+ sceneModes={[
175
+ CesiumSceneMode.SCENE3D,
176
+ CesiumSceneMode.SCENE2D,
177
+ CesiumSceneMode.COLUMBUS_VIEW,
178
+ ]}
175
179
  baseMaps={BASE_MAPS}
176
180
  >
177
181
  <CesiumXYZLayer options={optionsXYZSanDiego} />
@@ -1,5 +1,10 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
- import { Cartesian3, Math as CesiumMath, WebMercatorProjection, ScreenSpaceEventType } from 'cesium';
2
+ import {
3
+ Cartesian3,
4
+ Math as CesiumMath,
5
+ WebMercatorProjection,
6
+ ScreenSpaceEventType,
7
+ } from 'cesium';
3
8
  import { CesiumViewer, useCesiumMap } from '../map';
4
9
 
5
10
  import './coordinates-tracker.tool.css';
@@ -160,7 +160,10 @@ export const ScaleTrackerTool: React.FC<RScaleTrackerToolProps> = (props) => {
160
160
 
161
161
  return (): void => {
162
162
  try {
163
- mapViewer.camera.moveEnd.removeEventListener(setFromEvent);
163
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition*/
164
+ if (get(mapViewer, '_cesiumWidget') != undefined) {
165
+ mapViewer.camera.moveEnd.removeEventListener(setFromEvent);
166
+ }
164
167
  } catch (e) {
165
168
  console.log('CESIUM camera "moveEnd" remove listener failed', e);
166
169
  }
@@ -0,0 +1,19 @@
1
+ .zoomLevel {
2
+ z-index: 2000;
3
+ background-color: var(--mdc-theme-surface);
4
+ border-radius: 2px;
5
+ padding: 2px;
6
+ height: 24px;
7
+ }
8
+
9
+ .zoomLevelValue {
10
+ margin: 2px;
11
+ font-size: 14px;
12
+ line-height: 10px;
13
+ text-align: center;
14
+ }
15
+
16
+ .zoomLevelLabel {
17
+ font-size: 8px;
18
+ line-height: 8px;
19
+ }
@@ -0,0 +1,142 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { get } from 'lodash';
3
+ import { PerspectiveOffCenterFrustum } from 'cesium';
4
+ import { CesiumViewer, useCesiumMap } from '../map';
5
+ import { CesiumSceneMode } from '../map.types';
6
+
7
+ import './zoom_level-tracker.tool.css';
8
+
9
+ export interface RZoomLevelTrackerToolProps {
10
+ locale?: { [key: string]: string };
11
+ }
12
+
13
+ /* eslint-disable @typescript-eslint/no-magic-numbers, @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
14
+ const detectZoomLevel = (distance: number, viewer: CesiumViewer) => {
15
+ const MAX_ZOOM_LEVEL = 19;
16
+ const tileProvider = get(viewer.scene.globe, '_surface.tileProvider') as any;
17
+ const quadtree = tileProvider._quadtree;
18
+ const drawingBufferHeight = viewer.canvas.height;
19
+ const sseDenominator = get(viewer.camera.frustum, 'sseDenominator');
20
+
21
+ for (let level = 0; level <= MAX_ZOOM_LEVEL; level++) {
22
+ const maxGeometricError = tileProvider.getLevelMaximumGeometricError(level);
23
+ const error =
24
+ (maxGeometricError * drawingBufferHeight) / (distance * sseDenominator);
25
+ if (error < quadtree.maximumScreenSpaceError) {
26
+ return level;
27
+ }
28
+ }
29
+
30
+ return null;
31
+ };
32
+
33
+ const getZoomLevelHeights = (precision: number, viewer: CesiumViewer) => {
34
+ precision = precision || 10;
35
+
36
+ const result = [];
37
+ let step = 100000.0;
38
+ let currentZoomLevel = 0;
39
+ for (let height = 100000000.0; height > step; height = height - step) {
40
+ const level = detectZoomLevel(height, viewer);
41
+ if (level === null) {
42
+ break;
43
+ }
44
+
45
+ if (level !== currentZoomLevel) {
46
+ let minHeight = height;
47
+ let maxHeight = height + step;
48
+ while (maxHeight - minHeight > precision) {
49
+ height = minHeight + (maxHeight - minHeight) / 2;
50
+ if (detectZoomLevel(height, viewer) === level) {
51
+ minHeight = height;
52
+ } else {
53
+ maxHeight = height;
54
+ }
55
+ }
56
+
57
+ result.push({
58
+ level: level,
59
+ height: Math.round(height),
60
+ });
61
+
62
+ currentZoomLevel = level;
63
+
64
+ if (result.length >= 2) {
65
+ step = (result[result.length - 2].height - height) / 1000.0;
66
+ }
67
+ }
68
+ }
69
+
70
+ return result;
71
+ };
72
+ /* eslint-enable @typescript-eslint/no-magic-numbers */
73
+
74
+ export const ZoomLevelTrackerTool: React.FC<RZoomLevelTrackerToolProps> = (
75
+ props
76
+ ) => {
77
+ const mapViewer: CesiumViewer = useCesiumMap();
78
+ const [zoomLevel, setZoomLevel] = useState(1);
79
+ const zoomLevelHeights = getZoomLevelHeights(1, mapViewer);
80
+
81
+ useEffect(() => {
82
+ const calculateZoomLevel = () => {
83
+ const camera = mapViewer.camera;
84
+ const ORTHOPHOTO_HEIGHT_FRUSTRUM_FACTOR = 0.5;
85
+ let cameraHeight = 0;
86
+
87
+ switch (mapViewer.scene.mode) {
88
+ case CesiumSceneMode.SCENE3D:
89
+ cameraHeight = mapViewer.scene.mapProjection.ellipsoid.cartesianToCartographic(
90
+ camera.positionWC
91
+ ).height;
92
+ break;
93
+ case CesiumSceneMode.SCENE2D:
94
+ cameraHeight =
95
+ ((camera.frustum as PerspectiveOffCenterFrustum).right -
96
+ (camera.frustum as PerspectiveOffCenterFrustum).left) *
97
+ ORTHOPHOTO_HEIGHT_FRUSTRUM_FACTOR;
98
+ break;
99
+ case CesiumSceneMode.COLUMBUS_VIEW:
100
+ cameraHeight = camera.position.z;
101
+ break;
102
+ default:
103
+ cameraHeight = 0;
104
+ break;
105
+ }
106
+
107
+ const closestZoom = zoomLevelHeights.reduce((a, b) => {
108
+ return Math.abs(b.height - cameraHeight) <
109
+ Math.abs(a.height - cameraHeight)
110
+ ? b
111
+ : a;
112
+ });
113
+
114
+ setZoomLevel(closestZoom.level);
115
+ };
116
+
117
+ mapViewer.camera.moveEnd.addEventListener(calculateZoomLevel);
118
+
119
+ return (): void => {
120
+ try {
121
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition*/
122
+ if (get(mapViewer, '_cesiumWidget') != undefined) {
123
+ mapViewer.camera.moveEnd.removeEventListener(calculateZoomLevel);
124
+ }
125
+ } catch (e) {
126
+ console.log(
127
+ 'CESIUM camera "moveEnd"(from zoom tracker) remove listener failed',
128
+ e
129
+ );
130
+ }
131
+ };
132
+ }, [mapViewer]);
133
+
134
+ return (
135
+ <div className="zoomLevel">
136
+ <div className="zoomLevelValue">{zoomLevel}</div>
137
+ <div className="zoomLevelLabel">
138
+ {get(props.locale, 'ZOOM_LABEL') ?? 'zoom'}
139
+ </div>
140
+ </div>
141
+ );
142
+ };