Skip to content

Commit

Permalink
feat(globe): support single image layers (#486)
Browse files Browse the repository at this point in the history
* feat(globe): add support for single image layers

* chore(storage): remove tags from debug story

* fix(globe): fix eslint warning

* fix(use-globe-layer): use requestAnimationFrame only for single image layers
  • Loading branch information
pwambach authored Aug 18, 2020
1 parent 57f7f3c commit a40a35f
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 151 deletions.
2 changes: 1 addition & 1 deletion src/scripts/components/layers/time-slider/time-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import setGlobeTime from '../../../actions/set-globe-time';
import {getTimeRanges} from '../../../libs/get-time-ranges';
import {State} from '../../../reducers';
import {selectedLayerIdsSelector} from '../../../selectors/layers/selected-ids';
import {getLayerTimeIndex} from '../../../libs/get-layer-tile-url';
import {getLayerTimeIndex} from '../../../libs/get-image-layer-data';
import TimeSliderRange from '../time-slider-range/time-slider-range';
import TimePlayback from '../time-playback/time-playback';
import Button from '../../main/button/button';
Expand Down
73 changes: 6 additions & 67 deletions src/scripts/components/main/globe/globe.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import React, {FunctionComponent, useRef, useEffect, useState} from 'react';
import 'cesium/Build/Cesium/Widgets/widgets.css';
import {
Viewer,
SceneMode,
Color,
GeographicTilingScheme,
TileMapServiceImageryProvider,
UrlTemplateImageryProvider,
TextureMinificationFilter,
TextureMagnificationFilter
} from 'cesium';
import {Viewer, SceneMode, Color, TileMapServiceImageryProvider} from 'cesium';

import {
getGlobeView,
Expand All @@ -22,10 +13,12 @@ import {GlobeView} from '../../../types/globe-view';
import {GlobeProjection} from '../../../types/globe-projection';
import config from '../../../config/main';
import {useMarkers} from '../../../hooks/use-markers';
import {useGlobeLayer} from '../../../hooks/use-globe-layer';

import {GlobeProjectionState} from '../../../types/globe-projection-state';
import {BasemapId} from '../../../types/basemap';
import {Marker} from '../../../types/marker-type';
import {GlobeImageLayerData} from '../../../types/globe-image-layer-data';

import styles from './globe.styl';

Expand All @@ -50,9 +43,8 @@ interface Props {
active: boolean;
view: GlobeView;
projectionState: GlobeProjectionState;
tilesUrl: string | null;
imageLayer: GlobeImageLayerData | null;
basemap: BasemapId | null;
zoomLevels: number;
flyTo: GlobeView | null;
markers?: Marker[];
onMouseEnter: () => void;
Expand All @@ -77,9 +69,8 @@ function getBasemapUrl(id: BasemapId | null) {
const Globe: FunctionComponent<Props> = ({
view,
projectionState,
tilesUrl,
imageLayer,
basemap,
zoomLevels,
active,
flyTo,
markers = [],
Expand Down Expand Up @@ -207,59 +198,7 @@ const Globe: FunctionComponent<Props> = ({
setGlobeView(viewer, view);
}, [viewer, view, active]);

// update layer image when url changes
useEffect(() => {
if (!viewer) {
return;
}

const layers = viewer.scene.imageryLayers;

if (tilesUrl) {
const imageProvider = new UrlTemplateImageryProvider({
url: tilesUrl,
tilingScheme: new GeographicTilingScheme(),
minimumLevel: 0,
maximumLevel: zoomLevels - 1,
tileWidth: 256,
tileHeight: 256
});

imageProvider.readyPromise.then(() => {
const newLayer = viewer.scene.imageryLayers.addImageryProvider(
imageProvider
);
// @ts-ignore
newLayer.minificationFilter = TextureMinificationFilter.NEAREST;
// @ts-ignore
newLayer.magnificationFilter = TextureMagnificationFilter.NEAREST;
newLayer.alpha = 1;

// remove and destroy old layers if they exist
// we do not clean it up in the useEffect clean function because we want
// to wait until the new layer is ready to prevent flickering
const layersToRemove: Cesium.ImageryLayer[] = [];

for (let i = 0; i < layers.length; i++) {
const layer = layers.get(i);
if (i !== 0 && layer !== newLayer) {
layersToRemove.push(layer);
}
}

setTimeout(() => {
// eslint-disable-next-line max-nested-callbacks
layersToRemove.forEach(layer => layers.remove(layer, true));
}, 500);
});
} else if (layers.length > 1) {
// remove old layers when no image should be shown anymore
for (let i = 1; i < layers.length; i++) {
const layer = layers.get(i);
layers.remove(layer, true);
}
}
}, [viewer, tilesUrl, zoomLevels]);
useGlobeLayer(viewer, imageLayer);

// update basemap
useEffect(() => {
Expand Down
21 changes: 13 additions & 8 deletions src/scripts/components/main/globes/globes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, {
FunctionComponent,
useState,
useEffect,
useCallback
useCallback,
useMemo
} from 'react';
import {useSelector, useDispatch} from 'react-redux';

Expand All @@ -14,7 +15,7 @@ import {flyToSelector} from '../../../selectors/fly-to';
import setGlobeViewAction from '../../../actions/set-globe-view';
import Globe from '../globe/globe';
import LayerLegend from '../../layers/layer-legend/layer-legend';
import {getLayerTileUrl} from '../../../libs/get-layer-tile-url';
import {getImageLayerData} from '../../../libs/get-image-layer-data';
import {State} from '../../../reducers';
import {layerDetailsSelector} from '../../../selectors/layers/layer-details';
import {selectedLayerIdsSelector} from '../../../selectors/layers/selected-ids';
Expand Down Expand Up @@ -62,8 +63,14 @@ const Globes: FunctionComponent<Props> = ({markers = []}) => {
[dispatch]
);

const mainTilesUrl = getLayerTileUrl(mainLayerDetails, time);
const compareTilesUrl = getLayerTileUrl(compareLayerDetails, time);
const mainImageLayer = useMemo(
() => getImageLayerData(mainLayerDetails, time),
[mainLayerDetails, time]
);
const compareImageLayer = useMemo(
() => getImageLayerData(compareLayerDetails, time),
[compareLayerDetails, time]
);

// apply changes in the app state view to our local view copy
// we don't use the app state view all the time to keep store updates low
Expand Down Expand Up @@ -93,9 +100,8 @@ const Globes: FunctionComponent<Props> = ({markers = []}) => {
active={isMainActive}
view={currentView}
projectionState={projectionState}
tilesUrl={mainTilesUrl}
imageLayer={mainImageLayer}
basemap={mainLayerDetails?.basemap || null}
zoomLevels={mainLayerDetails?.zoomLevels || 0}
flyTo={flyTo}
onMouseEnter={() => setIsMainActive(true)}
onTouchStart={() => setIsMainActive(true)}
Expand All @@ -108,9 +114,8 @@ const Globes: FunctionComponent<Props> = ({markers = []}) => {
active={!isMainActive}
view={currentView}
projectionState={projectionState}
tilesUrl={compareTilesUrl}
imageLayer={compareImageLayer}
basemap={compareLayerDetails?.basemap || null}
zoomLevels={compareLayerDetails?.zoomLevels || 0}
flyTo={flyTo}
onMouseEnter={() => setIsMainActive(false)}
onTouchStart={() => setIsMainActive(false)}
Expand Down
1 change: 1 addition & 0 deletions src/scripts/config/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default {
layers: `${baseUrlStorage}/layers/layers-{lang}.json`,
layer: `${baseUrlTiles}/{id}/metadata.json`,
layerTiles: `${baseUrlTiles}/{id}/tiles/{timeIndex}/{z}/{x}/{reverseY}.png`,
layerImage: `${baseUrlTiles}/{id}/tiles/{timeIndex}/full.png`,
layerOfflinePackage: `${baseUrlTiles}/{id}/package.zip`,
storyOfflinePackage: `${baseUrlStorage}/stories/{id}/package.zip`,
storyMediaBase: `${baseUrlStorage}/stories/{id}`,
Expand Down
99 changes: 99 additions & 0 deletions src/scripts/hooks/use-globe-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {useEffect} from 'react';
import {
GeographicTilingScheme,
UrlTemplateImageryProvider,
TextureMinificationFilter,
TextureMagnificationFilter,
SingleTileImageryProvider
} from 'cesium';

import {GlobeImageLayerData} from '../types/globe-image-layer-data';
import {GlobeLayerType} from '../types/globe-layer-type';

// update layer image when url changes
export function useGlobeLayer(
viewer: Cesium.Viewer | null,
imageLayer: GlobeImageLayerData | null
) {
useEffect(() => {
if (!viewer) {
return;
}

const layers = viewer.scene.imageryLayers;

if (imageLayer) {
const imageryProvider = getImageProvider(imageLayer);

imageryProvider.readyPromise.then(() => {
const newLayer = viewer.scene.imageryLayers.addImageryProvider(
imageryProvider
);

// @ts-ignore
newLayer.minificationFilter = TextureMinificationFilter.NEAREST;
// @ts-ignore
newLayer.magnificationFilter = TextureMagnificationFilter.NEAREST;
newLayer.alpha = 1;

// remove and destroy old layers if they exist
// we do not clean it up in the useEffect clean function because we want
// to wait until the new layer is ready to prevent flickering
const layersToRemove: Cesium.ImageryLayer[] = [];

for (let i = 0; i < layers.length; i++) {
const layer = layers.get(i);
if (i !== 0 && layer !== newLayer) {
layersToRemove.push(layer);
}
}

const cleanAndCache = () => {
// eslint-disable-next-line max-nested-callbacks
layersToRemove.forEach(layer => layers.remove(layer, true));

// preload next images
if (imageLayer.type === GlobeLayerType.Image) {
preloadNext(imageLayer.nextUrls);
}
};

if (imageLayer.type === GlobeLayerType.Tiles) {
setTimeout(cleanAndCache, 500);
} else {
requestAnimationFrame(cleanAndCache);
}
});
} else if (layers.length > 1) {
// remove old layers when no image should be shown anymore (except base map)
removeAllLayers(layers);
}
}, [viewer, imageLayer]);
}

function getImageProvider(imageLayer: GlobeImageLayerData) {
return imageLayer.type === GlobeLayerType.Tiles
? new UrlTemplateImageryProvider({
url: imageLayer.url,
tilingScheme: new GeographicTilingScheme(),
minimumLevel: 0,
maximumLevel: imageLayer.zoomLevels - 1,
tileWidth: 256,
tileHeight: 256
})
: new SingleTileImageryProvider({url: imageLayer.url});
}

function removeAllLayers(layers: Cesium.ImageryLayerCollection) {
for (let i = 1; i < layers.length; i++) {
const layer = layers.get(i);
layers.remove(layer, true);
}
}

function preloadNext(urls: string[]) {
urls.forEach(url => {
const image = new Image();
image.src = url;
});
}
30 changes: 20 additions & 10 deletions src/scripts/libs/electron/get-offline-tiles-url.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import {GlobeLayerType} from '../../types/globe-layer-type';

// Returns the url template for offline usage
export function getOfflineTilesUrl(): string {
export function getOfflineTilesUrl(type: GlobeLayerType): string {
if (!window.cfs) {
console.error('Calling electron function from a non-electron environment');
return '';
}

return window.cfs.getDownloadsPath(
'downloads',
'{id}',
'tiles',
'{timeIndex}',
'{z}',
'{x}',
'{reverseY}.png'
);
return type === GlobeLayerType.Tiles
? window.cfs.getDownloadsPath(
'downloads',
'{id}',
'tiles',
'{timeIndex}',
'{z}',
'{x}',
'{reverseY}.png'
)
: window.cfs.getDownloadsPath(
'downloads',
'{id}',
'tiles',
'{timeIndex}',
'full.png'
);
}
Loading

0 comments on commit a40a35f

Please sign in to comment.