diff --git a/applications/docs/src/stories/layers/vector/default.stories.tsx b/applications/docs/src/stories/layers/vector/default.stories.tsx index d39b7bd..31f6069 100644 --- a/applications/docs/src/stories/layers/vector/default.stories.tsx +++ b/applications/docs/src/stories/layers/vector/default.stories.tsx @@ -82,36 +82,22 @@ Default.args = { type: 'vector', source: { type: 'vector', - url: 'mapbox://mapbox.country-boundaries-v1', + tiles: ['https://vectortileservices3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Santa_Monica_Mountains_Parcels_VTL/VectorTileServer/tile/{z}/{y}/{x}.pbf'], }, render: { layers: [ { interactive: true, type: 'fill', - 'source-layer': 'country_boundaries', + 'source-layer': 'Santa_Monica_Mountains_Parcels', paint: { - 'fill-color': [ - 'match', - ['get', 'region'], - 'Africa', - '#fbb03b', - 'Americas', - '#223b53', - 'Europe', - '#e55e5e', - 'Asia', - '#3bb2d0', - 'Oceania', - '#ffcc00', - /* other */ '#ccc' - ], + 'fill-color': '#ccc' } }, { interactive: true, type: 'line', - 'source-layer': 'country_boundaries', + 'source-layer': 'Santa_Monica_Mountains_Parcels', paint: { 'line-color': '#000', 'line-width': 1, diff --git a/applications/docs/src/stories/playground/apng-layer/deck-presentation.stories.tsx b/applications/docs/src/stories/playground/apng-layer/deck-presentation.stories.tsx new file mode 100644 index 0000000..92fc0d8 --- /dev/null +++ b/applications/docs/src/stories/playground/apng-layer/deck-presentation.stories.tsx @@ -0,0 +1,394 @@ + +import React, { useCallback, useMemo, useState } from 'react'; +import { Story } from '@storybook/react/types-6-0'; +// Layer manager +import { LayerManager, Layer, LayerProps } from '@vizzuality/layer-manager-react'; +import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; +import CartoProvider from '@vizzuality/layer-manager-provider-carto'; + +import GL from '@luma.gl/constants'; +import { MapboxLayer } from '@deck.gl/mapbox'; +import { TileLayer } from '@deck.gl/geo-layers'; +import { BitmapLayer } from '@deck.gl/layers'; +import { DecodedLayer } from '@vizzuality/layer-manager-layers-deckgl'; + +import parseAPNG from 'apng-js'; + +// Map +import Map from '../../../components/map'; +import useInterval from '../../layers/deck/utils'; + +const cartoProvider = new CartoProvider(); + +export default { + title: 'Playground/APNG-Layer', + argTypes: { + }, +}; + +const Template: Story = (args: LayerProps) => { + const [frame, setFrame] = useState(0); + const [delay, setDelay] = useState(null); + const [lossVisible, setLossVisible] = useState(true); + const minZoom = 0; + const maxZoom = 20; + const [viewport, setViewport] = useState({}); + const [bounds] = useState({ + bbox: [48.831181, -13.983606, 48.97221, -13.856368], + options: { + duration: 0, + } + }); + + useInterval(() => { + // 2001-2021 + const f = (frame === 20 - 1) ? 0 : frame + 1; + + setFrame(f); + }, delay); + + const DECK_LAYERS = useMemo(() => { + return [ + new MapboxLayer( + { + id: `prediction-animated`, + type: TileLayer, + frame, + getPolygonOffset: () => { + return [0, -50]; + }, + + + getTileData: (tile) => { + const { x, y, z, signal } = tile; + const url = `https://storage.googleapis.com/geo-ai/Redes/Tiles/Tsaratanana/APNGs/Prediction/${z}/${x}/${y}.png`; + const response = fetch(url, { signal }); + + if (signal.aborted) { + return null; + } + + return response + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const apng = parseAPNG(buffer); + if (apng instanceof Error) { + throw apng; + } + + return apng.frames.map((frame) => { + return { + ...frame, + bitmapData: createImageBitmap(frame.imageData), + }; + }); + }); + }, + tileSize: 256, + visible: true, + opacity: 1, + refinementStrategy: 'no-overlap', + renderSubLayers: (sl) => { + if (!sl) return null; + + const { + id: subLayerId, + data, + tile, + visible, + opacity = 1, + frame: f + } = sl; + + if (!tile || !data) return null; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + const FRAME = data[f]; + + if (FRAME) { + return new BitmapLayer({ + id: subLayerId, + image: FRAME.bitmapData, + bounds: [west, south, east, north], + getPolygonOffset: () => { + return [0, -50]; + }, + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity, + }); + } + return null; + }, + minZoom: 10, + maxZoom: 14, + extent: bounds.bbox, + } + ), + new MapboxLayer( + { + id: `deck-loss-raster-decode-animated`, + type: TileLayer, + // data: 'https://storage.googleapis.com/wri-public/Hansen_16/tiles/hansen_world/v1/tc30/{z}/{x}/{y}.png', + data: 'https://tiles.globalforestwatch.org/umd_tree_cover_loss/v1.9/tcd_30/{z}/{x}/{y}.png', + tileSize: 256, + visible: true, + opacity: lossVisible ? 0.35 : 0, + refinementStrategy: 'no-overlap', + decodeParams: { + startYear: 2001, + endYear: 2001 + frame, + }, + getPolygonOffset: () => { + return [0, -100]; + }, + + renderSubLayers: (sl) => { + const { + id: subLayerId, + data, + tile, + visible, + opacity: _opacity, + decodeParams: _decodeParams, + } = sl; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + if (data) { + return new DecodedLayer({ + id: subLayerId, + image: data, + bounds: [west, south, east, north], + getPolygonOffset: () => { + return [0, -100]; + }, + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity: _opacity, + decodeParams: _decodeParams, + decodeFunction: ` + // values for creating power scale, domain (input), and range (output) + float domainMin = 0.; + float domainMax = 255.; + float rangeMin = 0.; + float rangeMax = 255.; + + float exponent = zoom < 13. ? 0.3 + (zoom - 3.) / 20. : 1.; + float intensity = color.r * 255.; + + // get the min, max, and current values on the power scale + float minPow = pow(domainMin, exponent - domainMin); + float maxPow = pow(domainMax, exponent); + float currentPow = pow(intensity, exponent); + + // get intensity value mapped to range + float scaleIntensity = ((currentPow - minPow) / (maxPow - minPow) * (rangeMax - rangeMin)) + rangeMin; + // a value between 0 and 255 + alpha = zoom < 13. ? scaleIntensity / 255. : color.g; + + float year = 2000.0 + (color.b * 255.); + // map to years + if (year >= startYear && year <= endYear && year >= 2001.) { + color.r = 220. / 255.; + color.g = (72. - zoom + 102. - 3. * scaleIntensity / zoom) / 255.; + color.b = (33. - zoom + 153. - intensity / zoom) / 255.; + } else { + alpha = 0.; + } + ` + }); + } + return null; + }, + minZoom: 3, + maxZoom: 12, + } + ) + ] + }, [frame, lossVisible]); + + const handleViewportChange = useCallback((vw) => { + setViewport(vw); + }, []); + + return ( +
+
+ + { + setDelay(null); + setFrame(+e.target.value - 2001); + }} + /> + + {2001 + frame} + +
+ +
+
+ + { + setLossVisible(e.target.checked) + console.log(e.target.checked); + }} + /> +
+
+ + {(map) => ( + <> + + + + + + )} + +
+ ); +}; + + + +export const Presentation = Template.bind({}); +Presentation.args = { + id: 'presentation-layer', + type: 'deck', + source: { + parse: false, + }, + render: { + parse: false + }, + deck: [] +}; diff --git a/applications/docs/src/stories/playground/apng-layer/kigali.stories.tsx b/applications/docs/src/stories/playground/apng-layer/kigali.stories.tsx new file mode 100644 index 0000000..5e6b830 --- /dev/null +++ b/applications/docs/src/stories/playground/apng-layer/kigali.stories.tsx @@ -0,0 +1,389 @@ + +import React, { useCallback, useMemo, useState } from 'react'; +import { Story } from '@storybook/react/types-6-0'; +// Layer manager +import { LayerManager, Layer, LayerProps } from '@vizzuality/layer-manager-react'; +import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; +import CartoProvider from '@vizzuality/layer-manager-provider-carto'; + +import GL from '@luma.gl/constants'; +import { MapboxLayer } from '@deck.gl/mapbox'; +import { TileLayer } from '@deck.gl/geo-layers'; +import { BitmapLayer } from '@deck.gl/layers'; + +import parseAPNG from 'apng-js'; + +// Map +import Map from '../../../components/map'; +import useInterval from '../../layers/deck/utils'; + +const cartoProvider = new CartoProvider(); + +export default { + title: 'Playground/APNG-Layer', + argTypes: { + }, +}; + +const Template: Story = (args: LayerProps) => { + const [biiOpacity, setBiiOpacity] = useState(1); + const [biiChangeOpacity, setBiiChangeOpacity] = useState(0); + + + const [frame, setFrame] = useState(0); + const [delay, setDelay] = useState(null); + const minZoom = 10; + const maxZoom = 14; + const [viewport, setViewport] = useState({}); + const [bounds] = useState({ + bbox: [29.882812499999986, -2.1088986592431382, 30.5859375, -1.75753681130829994], + options: { + duration: 0, + } + }); + + useInterval(() => { + // 2017-2020 + const f = (frame === 4 - 1) ? 0 : frame + 1; + + setFrame(f); + }, delay); + + const DECK_LAYERS = useMemo(() => { + return [ + new MapboxLayer( + { + id: `prediction-animated`, + type: TileLayer, + frame, + getPolygonOffset: () => { + return [0, -50]; + }, + + + getTileData: (tile) => { + const { x, y, z, signal } = tile; + const url = `https://storage.googleapis.com/geo-ai/Redes/Tiles/Kigali/APNGs/Sentinel/${z}/${x}/${y}.png`; + const response = fetch(url, { signal }); + + if (signal.aborted) { + return null; + } + + return response + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const apng = parseAPNG(buffer); + if (apng instanceof Error) { + throw apng; + } + + return apng.frames.map((frame) => { + return { + ...frame, + bitmapData: createImageBitmap(frame.imageData), + }; + }); + }); + }, + tileSize: 256, + visible: true, + opacity: 1, + refinementStrategy: 'no-overlap', + renderSubLayers: (sl) => { + if (!sl) return null; + + const { + id: subLayerId, + data, + tile, + visible, + opacity = 1, + frame: f + } = sl; + + if (!tile || !data) return null; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + const FRAME = data[f]; + + if (FRAME) { + return new BitmapLayer({ + id: subLayerId, + image: FRAME.bitmapData, + bounds: [west, south, east, north], + getPolygonOffset: () => { + return [0, -50]; + }, + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity, + }); + } + return null; + }, + minZoom: 10, + maxZoom: 14, + extent: bounds.bbox, + } + ), + new MapboxLayer( + { + id: `BII-animated`, + type: TileLayer, + frame, + getPolygonOffset: () => { + return [0, -50]; + }, + + + getTileData: (tile) => { + const { x, y, z, signal } = tile; + const url = `https://storage.googleapis.com/geo-ai/Redes/Tiles/Kigali/BII/APNGs/${z}/${x}/${y}.png`; + const response = fetch(url, { signal }); + + if (signal.aborted) { + return null; + } + + return response + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const apng = parseAPNG(buffer); + if (apng instanceof Error) { + throw apng; + } + + return apng.frames.map((frame) => { + return { + ...frame, + bitmapData: createImageBitmap(frame.imageData), + }; + }); + }); + }, + tileSize: 256, + visible: true, + opacity: biiOpacity, + refinementStrategy: 'no-overlap', + renderSubLayers: (sl) => { + if (!sl) return null; + + const { + id: subLayerId, + data, + tile, + visible, + opacity = 1, + frame: f + } = sl; + + if (!tile || !data) return null; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + const FRAME = data[f]; + + if (FRAME) { + return new BitmapLayer({ + id: subLayerId, + image: FRAME.bitmapData, + bounds: [west, south, east, north], + getPolygonOffset: () => { + return [0, -50]; + }, + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity, + }); + } + return null; + }, + minZoom: 10, + maxZoom: 14, + extent: bounds.bbox, + } + ), + ] + }, [frame, biiOpacity]); + + const handleViewportChange = useCallback((vw) => { + setViewport(vw); + }, []); + + return ( +
+ {/* Timeline */} +
+ + { + setDelay(null); + setFrame(+e.target.value - 2017); + }} + /> + + {2017 + frame} + +
+ + {/* Layers */} +
+
+ + { + setBiiOpacity(e.target.checked ? 1 : 0); + }} + /> +
+
+ + { + setBiiChangeOpacity(e.target.checked ? 1 : 0); + }} + /> +
+
+ + + + {(map) => ( + <> + + {/* */} + + + + + )} + +
+ ); +}; + + + +export const Kigali = Template.bind({}); +Kigali.args = { + id: 'kigali-layer', + type: 'deck', + source: { + parse: false, + }, + render: { + parse: false + }, + deck: [] +}; diff --git a/applications/docs/src/stories/playground/apng-layer/tsaratanana.stories.tsx b/applications/docs/src/stories/playground/apng-layer/tsaratanana.stories.tsx new file mode 100644 index 0000000..f7d7831 --- /dev/null +++ b/applications/docs/src/stories/playground/apng-layer/tsaratanana.stories.tsx @@ -0,0 +1,417 @@ + +import React, { useCallback, useMemo, useState } from 'react'; +import { Story } from '@storybook/react/types-6-0'; +// Layer manager +import { LayerManager, Layer, LayerProps } from '@vizzuality/layer-manager-react'; +import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; +import CartoProvider from '@vizzuality/layer-manager-provider-carto'; + +import GL from '@luma.gl/constants'; +import { MapboxLayer } from '@deck.gl/mapbox'; +import { TileLayer } from '@deck.gl/geo-layers'; +import { BitmapLayer } from '@deck.gl/layers'; + +import parseAPNG from 'apng-js'; + +// Map +import Map from '../../../components/map'; +import useInterval from '../../layers/deck/utils'; + +const cartoProvider = new CartoProvider(); + +export default { + title: 'Playground/APNG-Layer', + argTypes: { + }, +}; + +const Template: Story = (args: LayerProps) => { + const [biiOpacity, setBiiOpacity] = useState(1); + const [biiChangeOpacity, setHumanFootprintOpacity] = useState(0); + + + const [frame, setFrame] = useState(0); + const [delay, setDelay] = useState(null); + const minZoom = 11; + const maxZoom = 14; + const [viewport, setViewport] = useState({}); + const [bounds] = useState({ + bbox: [48.69140625000001,-14.093957177836236,49.04296875,-13.752724664396975], + options: { + duration: 0, + } + }); + + useInterval(() => { + // 2017-2020 + const f = (frame === 4 - 1) ? 0 : frame + 1; + + setFrame(f); + }, delay); + + const DECK_LAYERS = useMemo(() => { + return [ + new MapboxLayer( + { + id: `prediction-animated`, + type: TileLayer, + frame, + getPolygonOffset: () => { + return [0, -50]; + }, + + + getTileData: (tile) => { + const { x, y, z, signal } = tile; + const url = `https://storage.googleapis.com/geo-ai/Redes/Tiles/Tsaratanana2/APNGs/Sentinel/${z}/${x}/${y}.png`; + const response = fetch(url, { signal }); + + if (signal.aborted) { + return null; + } + + return response + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const apng = parseAPNG(buffer); + if (apng instanceof Error) { + throw apng; + } + + return apng.frames.map((frame) => { + return { + ...frame, + bitmapData: createImageBitmap(frame.imageData), + }; + }); + }); + }, + tileSize: 256, + visible: true, + opacity: 1, + refinementStrategy: 'no-overlap', + renderSubLayers: (sl) => { + if (!sl) return null; + + const { + id: subLayerId, + data, + tile, + visible, + opacity = 1, + frame: f + } = sl; + + if (!tile || !data) return null; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + const FRAME = data[f]; + + if (FRAME) { + return new BitmapLayer({ + id: subLayerId, + image: FRAME.bitmapData, + bounds: [west, south, east, north], + getPolygonOffset: () => { + return [0, -50]; + }, + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity, + }); + } + return null; + }, + minZoom: 10, + maxZoom: 14, + extent: bounds.bbox, + } + ), + new MapboxLayer( + { + id: `BII-animated`, + type: TileLayer, + frame, + getPolygonOffset: () => { + return [0, -50]; + }, + + + getTileData: (tile) => { + const { x, y, z, signal } = tile; + const url = `https://storage.googleapis.com/geo-ai/Redes/Tiles/Tsaratanana/BII/APNGs/${z}/${x}/${y}.png`; + const response = fetch(url, { signal }); + + if (signal.aborted) { + return null; + } + + return response + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const apng = parseAPNG(buffer); + if (apng instanceof Error) { + throw apng; + } + + return apng.frames.map((frame) => { + return { + ...frame, + bitmapData: createImageBitmap(frame.imageData), + }; + }); + }); + }, + tileSize: 256, + visible: true, + opacity: biiOpacity, + refinementStrategy: 'no-overlap', + renderSubLayers: (sl) => { + if (!sl) return null; + + const { + id: subLayerId, + data, + tile, + visible, + opacity = 1, + frame: f + } = sl; + + if (!tile || !data) return null; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + const FRAME = data[f]; + + if (FRAME) { + return new BitmapLayer({ + id: subLayerId, + image: FRAME.bitmapData, + bounds: [west, south, east, north], + getPolygonOffset: () => { + return [0, -50]; + }, + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity, + }); + } + return null; + }, + minZoom: 10, + maxZoom: 14, + extent: bounds.bbox, + } + ), + ] + }, [frame, biiOpacity]); + + const handleViewportChange = useCallback((vw) => { + setViewport(vw); + }, []); + + return ( +
+ {/* Timeline */} +
+ + { + setDelay(null); + setFrame(+e.target.value - 2017); + }} + /> + + {2017 + frame} + +
+ + {/* Layers */} +
+
+ + { + setBiiOpacity(e.target.checked ? 1 : 0); + }} + /> +
+
+ + { + setHumanFootprintOpacity(e.target.checked ? 1 : 0); + }} + /> +
+
+ + + + {(map) => ( + <> + + + + + + + + )} + +
+ ); +}; + + + +export const Tsaratanana = Template.bind({}); +Tsaratanana.args = { + id: 'tsaratanana-layer', + type: 'deck', + source: { + parse: false, + }, + render: { + parse: false + }, + deck: [] +}; diff --git a/applications/docs/src/stories/playground/decoded-raster-layer/default.stories.tsx b/applications/docs/src/stories/playground/decoded-raster-layer/default.stories.tsx index 3c68afe..734cc58 100644 --- a/applications/docs/src/stories/playground/decoded-raster-layer/default.stories.tsx +++ b/applications/docs/src/stories/playground/decoded-raster-layer/default.stories.tsx @@ -169,7 +169,7 @@ const Template: Story = (args: any) => { minZoom={minZoom} maxZoom={maxZoom} viewState={viewport} - mapStyle="mapbox://styles/mapbox/light-v9" + mapStyle="mapbox://styles/layer-manager/cl7stzzqj004t14lfz0mhbkve" mapboxAccessToken={process.env.STORYBOOK_MAPBOX_API_TOKEN} onViewStateChange={handleViewportChange} > diff --git a/applications/docs/src/stories/playground/extensions/decode-extension.stories.tsx b/applications/docs/src/stories/playground/extensions/decode-extension.stories.tsx new file mode 100644 index 0000000..ad2b3d6 --- /dev/null +++ b/applications/docs/src/stories/playground/extensions/decode-extension.stories.tsx @@ -0,0 +1,278 @@ + +import React, { useCallback, useMemo, useState } from 'react'; +import { Story } from '@storybook/react/types-6-0'; +// Layer manager +import { LayerManager, Layer, LayerProps } from '@vizzuality/layer-manager-react'; +import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; +import CartoProvider from '@vizzuality/layer-manager-provider-carto'; + +import GL from '@luma.gl/constants'; +import { TileLayer } from '@deck.gl/geo-layers'; +import { BitmapLayer } from '@deck.gl/layers'; +import { MapboxLayer } from '@deck.gl/mapbox'; +import { LayerExtension } from '@deck.gl/core'; + +// Map +import Map from '../../../components/map'; + +const cartoProvider = new CartoProvider(); + +export default { + title: 'Playground/Extensions', + argTypes: { + deck: { + table: { + disable: true + } + }, + tileUrl: { + name: 'tileUrl', + type: { name: 'Tile URL', required: true }, + defaultValue: 'https://storage.googleapis.com/wri-public/Hansen_16/tiles/hansen_world/v1/tc30/{z}/{x}/{y}.png', + control: { + type: 'text' + }, + }, + decodeParams: { + name: 'decodeParams', + type: { name: 'object', required: true }, + defaultValue: { + startYear: 2001, + endYear: 2017, + } + }, + decodeFunction: { + name: 'decodeFunction', + type: { name: 'string', required: true }, + defaultValue: ``, + description: 'The decode function you will apply to each tile pixel', + control: { + type: 'text' + } + } + }, +}; + +class LossExtension extends LayerExtension { + getShaders() { + return { + inject: { + 'fs:#decl': ` + uniform float zoom; + uniform float startYear; + uniform float endYear; + `, + + 'fs:DECKGL_FILTER_COLOR': ` + ${this.props.decodeFunction} + ` + } + }; + } + + updateState({ props, changeFlags }) { + const { + decodeParams = {}, + zoom + } = props; + + if (changeFlags.extensionsChanged || changeFlags.somethingChanged.decodeFunction) { + const { gl } = this.context; + this.state.model?.delete(); + this.state.model = this._getModel(gl); + this.getAttributeManager().invalidateAll(); + } + + for (const model of this.getModels()) { + model.setUniforms({ + zoom, + ...decodeParams, + }); + } + } +} + +const Template: Story = (args: any) => { + const { id, tileUrl, decodeFunction, decodeParams } = args; + + const minZoom = 2; + const maxZoom = 20; + const [viewport, setViewport] = useState({}); + + const [bounds] = useState(null); + + const DECK_LAYERS = useMemo(() => { + return [ + new MapboxLayer( + { + id, + type: TileLayer, + data: tileUrl, + tileSize: 256, + visible: true, + opacity: 1, + refinementStrategy: 'no-overlap', + decodeFunction, + decodeParams, + renderSubLayers: (sl) => { + const { + id: subLayerId, + data, + tile, + visible, + opacity: _opacity, + decodeParams: dParams, + decodeFunction: dFunction, + } = sl; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + if (data) { + return new BitmapLayer({ + id: subLayerId, + image: data, + bounds: [west, south, east, north], + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity: _opacity, + decodeParams: dParams, + decodeFunction: dFunction, + extensions: [new LossExtension()], + updateTriggers: { + decodeParams: dParams, + decodeFunction: dFunction, + } + }); + } + return null; + }, + minZoom: 3, + maxZoom: 12, + } + ) + ] + }, [decodeFunction, decodeParams]); + + const handleViewportChange = useCallback((vw) => { + setViewport(vw); + }, []); + + return ( +
+ + {(map) => ( + + + + )} + +
+ ); +}; + +export const DecodedExtension = Template.bind({}); +DecodedExtension.args = { + id: 'deck-loss-raster-decode', + type: 'deck', + decodeFunction: `// values for creating power scale, domain (input), and range (output) + float domainMin = 0.; + float domainMax = 255.; + float rangeMin = 0.; + float rangeMax = 255.; + + float exponent = zoom < 13. ? 0.3 + (zoom - 3.) / 20. : 1.; + float intensity = color.r * 255.; + + // get the min, max, and current values on the power scale + float minPow = pow(domainMin, exponent - domainMin); + float maxPow = pow(domainMax, exponent); + float currentPow = pow(intensity, exponent); + + // get intensity value mapped to range + float scaleIntensity = ((currentPow - minPow) / (maxPow - minPow) * (rangeMax - rangeMin)) + rangeMin; + // a value between 0 and 255 + color.a = zoom < 13. ? scaleIntensity / 255. : color.g; + + float year = 2000.0 + (color.b * 255.); + + // map to years + if (year >= startYear && year <= endYear && year >= 2001.) { + color.r = 220. / 255.; + color.g = (72. - zoom + 102. - 3. * scaleIntensity / zoom) / 255.; + color.b = (33. - zoom + 153. - intensity / zoom) / 255.; + } else { + discard; + }` +}; + +export const LossByYear = Template.bind({}); +LossByYear.args = { + id: 'deck-loss-by-year-raster-decode', + type: 'deck', + decodeFunction: `// values for creating power scale, domain (input), and range (output) + float domainMin = 0.; + float domainMax = 255.; + float rangeMin = 0.; + float rangeMax = 255.; + + float exponent = zoom < 13. ? 0.3 + (zoom - 3.) / 20. : 1.; + float intensity = color.r * 255.; + + // get the min, max, and current values on the power scale + float minPow = pow(domainMin, exponent - domainMin); + float maxPow = pow(domainMax, exponent); + float currentPow = pow(intensity, exponent); + + // get intensity value mapped to range + float scaleIntensity = ((currentPow - minPow) / (maxPow - minPow) * (rangeMax - rangeMin)) + rangeMin; + // a value between 0 and 255 + color.a = zoom < 13. ? scaleIntensity / 255. : color.g; + + float year = 2000.0 + (color.b * 255.); + float totalYears = 2017. - 2001.; + float yearFraction = (year - 2001.) / totalYears; + + // map to years + if (year >= startYear && year <= endYear && year >= 2001.) { + float b = (33. - zoom + 153. - intensity / zoom) / 255.; + color.r = 220. / 255.; + color.g = (72. - zoom + 102. - 3. * scaleIntensity / zoom) / 255.; + color.b = mix(b, 0., yearFraction); + } else { + discard; + }` +}; \ No newline at end of file diff --git a/applications/docs/src/stories/playground/extensions/test-extension.stories.tsx b/applications/docs/src/stories/playground/extensions/test-extension.stories.tsx new file mode 100644 index 0000000..e1d556d --- /dev/null +++ b/applications/docs/src/stories/playground/extensions/test-extension.stories.tsx @@ -0,0 +1,290 @@ + +import React, { useCallback, useMemo, useState } from 'react'; +import { Story } from '@storybook/react/types-6-0'; +// Layer manager +import { LayerManager, Layer, LayerProps } from '@vizzuality/layer-manager-react'; +import PluginMapboxGl from '@vizzuality/layer-manager-plugin-mapboxgl'; +import CartoProvider from '@vizzuality/layer-manager-provider-carto'; + +import GL from '@luma.gl/constants'; +import { TileLayer } from '@deck.gl/geo-layers'; +import { BitmapLayer } from '@deck.gl/layers'; +import { MapboxLayer } from '@deck.gl/mapbox'; +import { LayerExtension } from '@deck.gl/core'; + +// Map +import Map from '../../../components/map'; + +const cartoProvider = new CartoProvider(); + +export default { + title: 'Playground/Extensions', + argTypes: { + deck: { + table: { + disable: true + } + }, + tileUrl: { + name: 'tileUrl', + type: { name: 'Tile URL', required: true }, + defaultValue: 'https://earthengine.google.org/static/hansen_2013/gain_alpha/{z}/{x}/{y}.png', + control: { + type: 'text' + }, + }, + decodeParams: { + name: 'decodeParams', + type: { name: 'object', required: true }, + defaultValue: { + startYear: 2001, + endYear: 2017, + } + }, + decodeFunction: { + name: 'decodeFunction', + type: { name: 'string', required: true }, + defaultValue: ``, + description: 'The decode function you will apply to each tile pixel', + control: { + type: 'text' + } + } + }, +}; + +class TestExtension extends LayerExtension { + getShaders() { + return { + inject: { + 'vs:#decl': ` + uniform float u_mouseLng; + uniform float u_mouseLat; + varying vec4 v_texWorld; + varying vec3 v_texWorldCommon; + varying vec4 v_mousePosition; + varying vec3 v_mousePositionCommon; + `, + 'vs:#main-end': ` + v_texWorld = project_position_to_clipspace(positions, positions64Low, vec3(0.0), geometry.position); + v_texWorldCommon = positions.xyz; + v_mousePosition = project_position_to_clipspace(vec3(u_mouseLng, u_mouseLat, 0.0), positions64Low, vec3(0.0)); + v_mousePositionCommon = vec3(u_mouseLng, u_mouseLat, 0.0); + `, + 'fs:#decl': ` + uniform float zoom; + uniform float startYear; + uniform float endYear; + uniform float u_mouseLng; + uniform float u_mouseLat; + varying vec4 v_texWorld; + varying vec3 v_texWorldCommon; + varying vec4 v_mousePosition; + + float circle(vec2 pt, vec2 center, float radius, float edge_thickness){ + vec2 p = pt - center; + float len = length(p); + float result = 1.0-smoothstep(radius-edge_thickness, radius, len); + + return result; + } + `, + + 'fs:#main-end': ` + ${this.props.decodeFunction} + ` + } + }; + } + + updateState({ props, changeFlags }) { + const { + decodeParams = {}, + zoom, + u_mouseLng, + u_mouseLat, + } = props; + + if (changeFlags.extensionsChanged || changeFlags.somethingChanged.decodeFunction) { + const { gl } = this.context; + this.state.model?.delete(); + this.state.model = this._getModel(gl); + this.getAttributeManager().invalidateAll(); + } + + for (const model of this.getModels()) { + console.log(u_mouseLat, u_mouseLng); + model.setUniforms({ + zoom, + u_mouseLng, + u_mouseLat, + ...decodeParams, + }); + } + } +} + +const Template: Story = (args: any) => { + const { id, tileUrl, decodeFunction, decodeParams } = args; + + const minZoom = 2; + const maxZoom = 20; + const [viewport, setViewport] = useState({}); + const [mouseLngLat, setMouseLngLat] = useState({ + lng: 0, + lat: 0 + }); + + const [bounds] = useState(null); + + const DECK_LAYERS = useMemo(() => { + return [ + new MapboxLayer( + { + id, + type: TileLayer, + data: tileUrl, + tileSize: 256, + visible: true, + opacity: 1, + refinementStrategy: 'no-overlap', + decodeFunction, + decodeParams, + u_mouseLng: mouseLngLat.lng, + u_mouseLat: mouseLngLat.lat, + renderSubLayers: (sl) => { + const { + id: subLayerId, + data, + tile, + visible, + u_mouseLng, + u_mouseLat, + opacity: _opacity, + decodeParams: dParams, + decodeFunction: dFunction, + } = sl; + + const { + z, + bbox: { + west, south, east, north, + }, + } = tile; + + if (data) { + return new BitmapLayer({ + id: subLayerId, + image: data, + bounds: [west, south, east, north], + textureParameters: { + [GL.TEXTURE_MIN_FILTER]: GL.NEAREST, + [GL.TEXTURE_MAG_FILTER]: GL.NEAREST, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, + }, + zoom: z, + visible, + opacity: _opacity, + u_mouseLng, + u_mouseLat, + decodeParams: dParams, + decodeFunction: dFunction, + extensions: [new TestExtension()], + updateTriggers: { + decodeParams: dParams, + decodeFunction: dFunction, + } + }); + } + return null; + }, + minZoom: 3, + maxZoom: 12, + } + ) + ] + }, [decodeFunction, decodeParams, mouseLngLat]); + + const handleViewportChange = useCallback((vw) => { + setViewport(vw); + }, []); + + const handleMouseMove = useCallback((e) => { + if (e.lngLat) { + setMouseLngLat(e.lngLat); + } + } ,[]); + + return ( +
+ + {(map) => ( + + + + )} + +
+ ); +}; + +export const TestVarying = Template.bind({}); +TestVarying.args = { + id: 'deck-loss-raster-decode', + type: 'deck', + decodeFunction: `// decode function + vec3 color = mix(bitmapColor.rgb, vec3(1.0,0.0,0.0), v_texWorld.x); + // vec3 color = mix(bitmapColor.rgb, vec3(1.0,0.0,0.0), (abs(v_texWorldCommon.x / 180.))); + gl_FragColor = vec4(color, bitmapColor.a); + ` +}; + +export const TestStep = Template.bind({}); +TestStep.args = { + id: 'deck-loss-raster-decode', + type: 'deck', + decodeFunction: `// decode function + float step = step(0., v_texWorldCommon.y); + vec3 color = mix(bitmapColor.rgb, vec3(1.0,0.0,0.0), step); + gl_FragColor = vec4(color, bitmapColor.a); + ` +}; + +export const TestCircleMovement = Template.bind({}); +TestCircleMovement.args = { + id: 'deck-loss-raster-decode', + type: 'deck', + decodeFunction: `// decode function + vec3 color = bitmapColor.rgb * circle(v_mousePosition.xy, v_mousePosition.xy, 0.5, 0.002); + gl_FragColor = vec4(color, bitmapColor.a); + // gl_FragColor = vec4(v_mousePosition.x, v_mousePosition.y, 0.0, bitmapColor.a); + + ` +}; + +