diff --git a/src/App.tsx b/src/App.tsx index ecc8c95..46e1b80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,12 @@ import { useCallback, useMemo, useState, useReducer, ChangeEvent } from 'react'; import { MapGroupResponse, SourceGroup, Box } from './types/maps'; import { ColorMapControls } from './components/ColorMapControls'; -import { fetchBoxes, fetchMaps, fetchSources } from './utils/fetchUtils'; +import { + fetchBoxes, + fetchMaps, + fetchSources, + getHistogramData, +} from './utils/fetchUtils'; import { assertInternalBaselayer, baselayersReducer, @@ -34,14 +39,35 @@ function App() { // map baselayers const { mapGroups, internalBaselayers } = await fetchMaps(); - // // If we end up with no maps for some reason, return early + // If we end up with no maps for some reason, return early if (!mapGroups.length || !internalBaselayers.length) return; - // Set the baselayersState with the finalBands; note that this action will also set the + // Get what will be the default baselayer's histogram data to set in the reducer state + const defaultInitialBaselayer = { ...internalBaselayers[0] }; + const histogramData = await getHistogramData( + defaultInitialBaselayer.layer_id + ); + + // Check if the default baselayer has an undefined vmin or vmax; if so, set the + // vmin and vmax for the baselayer + if ( + defaultInitialBaselayer.vmin === undefined || + defaultInitialBaselayer.vmax === undefined + ) { + const histogramData = await getHistogramData( + defaultInitialBaselayer.layer_id + ); + defaultInitialBaselayer.vmin = histogramData.vmin; + defaultInitialBaselayer.vmax = histogramData.vmax; + internalBaselayers[0] = defaultInitialBaselayer; + } + + // Set the baselayersState with the internalBaselayers; note that this action will also set the // activeBaselayer to be finalBands[0] dispatchBaselayersChange({ type: SET_BASELAYERS_STATE, internalBaselayers: internalBaselayers, + histogramData, }); return mapGroups; @@ -181,7 +207,8 @@ function App() { [baselayersState.activeBaselayer] ); - const { activeBaselayer, internalBaselayers } = baselayersState; + const { activeBaselayer, internalBaselayers, histogramData } = + baselayersState; return ( <> )} - {isAuthenticated !== null && assertInternalBaselayer(activeBaselayer) && ( - - )} + {isAuthenticated !== null && + assertInternalBaselayer(activeBaselayer) && + activeBaselayer.vmin !== undefined && + activeBaselayer.vmax !== undefined && + histogramData && ( + + )} ); } diff --git a/src/components/ColorMapControls.tsx b/src/components/ColorMapControls.tsx index fca29e7..e84260b 100644 --- a/src/components/ColorMapControls.tsx +++ b/src/components/ColorMapControls.tsx @@ -1,11 +1,10 @@ import { ChangeEventHandler, useCallback, - useEffect, useState, useMemo, + useEffect, } from 'react'; -import { SERVICE_URL } from '../configs/mapSettings'; import { CMAP_OPTIONS, HISTOGRAM_SIZE_X, @@ -17,6 +16,7 @@ import { ColorMapHistogram } from './ColorMapHistogram'; import { CustomColorMapDialog } from './CustomColorMapDialog'; import { safeLog } from '../utils/numberUtils'; import { getAbsoluteHistogramData } from '../utils/histogramUtils'; +import { getCmapImage } from '../utils/fetchUtils'; export type ColorMapControlsProps = { /** the selected or default min and max values for the slider */ @@ -43,6 +43,8 @@ export type ColorMapControlsProps = { onLogScaleChange: (checked: boolean) => void; /** handler to update isAbsoluteValue state and to set cmap values as necessary */ onAbsoluteValueChange: (checked: boolean) => void; + /** the histogramData that is fetched/cached with each baselayer change */ + histogramData: HistogramResponse; }; /** @@ -59,21 +61,29 @@ export function ColorMapControls(props: ColorMapControlsProps) { onCmapValuesChange, cmap, onCmapChange, - activeBaselayerId, units, quantity, isLogScale, isAbsoluteValue, onLogScaleChange, onAbsoluteValueChange, + histogramData, } = props; - const [cmapImage, setCmapImage] = useState(undefined); - const [histogramData, setHistogramData] = useState< - HistogramResponse | undefined - >(undefined); + const [cmapImage, setCmapImage] = useState(undefined); const [showCustomDialog, setShowCustomDialog] = useState(false); const [cmapOptions, setCmapOptions] = useState(CMAP_OPTIONS); + /** + * Fetch or retrieve from cache the cmap image when user changes cmap selection + */ + useEffect(() => { + async function getImage() { + const image = await getCmapImage(cmap); + setCmapImage(image); + } + getImage(); + }, [cmap]); + /** Processes the histogram data so that it's ready to create the polygon in ColorMapHistogram */ const processedHistogramData = useMemo(() => { if (histogramData) { @@ -122,27 +132,6 @@ export function ColorMapControls(props: ColorMapControlsProps) { } }, [histogramData, isLogScale, isAbsoluteValue]); - /** Fetch and set the URL to the color map image if/when cmap or its setter changes */ - useEffect(() => { - async function getCmapImage() { - const image = await fetch(`${SERVICE_URL}/histograms/${cmap}.png`); - setCmapImage(image.url); - } - getCmapImage(); - }, [cmap, setCmapImage]); - - /** Fetch and set the histogram data if/when the active layer and/or setHistogramData changes */ - useEffect(() => { - async function getHistogramData() { - const response = await fetch( - `${SERVICE_URL}/histograms/data/${activeBaselayerId}` - ); - const data: HistogramResponse = await response.json(); - setHistogramData(data); - } - getHistogramData(); - }, [activeBaselayerId, setHistogramData]); - /** Determines the min, max, and step attributes for the range slider. Min and max are found by comparing the user-controlled (or default) 'values' to the histogram's 'edges', which allows for the range slider to resize itself according to min/max values that may diff --git a/src/components/ColorMapHistogram.tsx b/src/components/ColorMapHistogram.tsx index daf3a39..29391f1 100644 --- a/src/components/ColorMapHistogram.tsx +++ b/src/components/ColorMapHistogram.tsx @@ -1,14 +1,14 @@ import { useMemo } from 'react'; import { PointArray } from '@svgdotjs/svg.js'; -import { HistogramResponse } from '../types/maps'; +import { HistogramData } from '../types/maps'; import { HISTOGRAM_SIZE_X, HISTOGRAM_SIZE_Y, } from '../configs/cmapControlSettings'; type Props = { - /** The data from the histogram response */ - data?: HistogramResponse; + /** The applicable histogram data from the histogram response */ + data?: HistogramData; /** The user's min and max values for the range slider to use as edgeStart or edgeEnd in the event the user sets these beyond the histogram's min or max edges */ userMinAndMaxValues: { min: number; max: number }; diff --git a/src/components/ColorMapSlider.tsx b/src/components/ColorMapSlider.tsx index 4bb2b4f..46580ba 100644 --- a/src/components/ColorMapSlider.tsx +++ b/src/components/ColorMapSlider.tsx @@ -8,6 +8,8 @@ import { import { ColorMapControlsProps } from './ColorMapControls'; import './styles/color-map-controls.css'; +let THUMB_KEY_COUNTER = 1; + interface ColorMapSliderProps extends Omit< ColorMapControlsProps, @@ -16,6 +18,7 @@ interface ColorMapSliderProps | 'onCmapChange' | 'onLogScaleChange' | 'onAbsoluteValueChange' + | 'histogramData' > { /** The URL to the color map image */ cmapImage?: string; @@ -301,7 +304,7 @@ export function ColorMapSlider(props: ColorMapSliderProps) { renderThumb={({ props, isDragged }) => (
t.get('id') === 'baselayer-' + activeBaselayer!.layer_id )!; + const activeLayerSource = activeLayer.getSource(); + activeLayerSource?.setUrl( + `${SERVICE_URL}/maps/${activeBaselayer.layer_id}/{z}/{-y}/{x}/tile.png?cmap=${activeBaselayer.cmap}&vmin=${activeBaselayer.isLogScale ? Math.pow(10, activeBaselayer.vmin!) : activeBaselayer.vmin}&vmax=${activeBaselayer.isLogScale ? Math.pow(10, activeBaselayer.vmax!) : activeBaselayer.vmax}&flip=${flipTiles}&log_norm=${activeBaselayer.isLogScale}&abs=${activeBaselayer.isAbsoluteValue}` + ); mapRef.current.addLayer(activeLayer); } else { const externalBaselayer = EXTERNAL_BASELAYERS.find( @@ -406,7 +442,7 @@ export function OpenLayersMap({ mapRef.current.addLayer(activeLayer); } } - }, [activeBaselayer, tileLayers, externalTileLayers]); + }, [activeBaselayer, tileLayers, externalTileLayers, flipTiles]); /** * Add keyboard support for switching baselayers diff --git a/src/reducers/baselayersReducer.ts b/src/reducers/baselayersReducer.ts index 24c043e..be08aaa 100644 --- a/src/reducers/baselayersReducer.ts +++ b/src/reducers/baselayersReducer.ts @@ -1,6 +1,7 @@ import { BaselayersState, ExternalBaselayer, + HistogramResponse, InternalBaselayer, } from '../types/maps'; import { safeLog } from '../utils/numberUtils'; @@ -20,6 +21,7 @@ export function assertInternalBaselayer( export const initialBaselayersState: BaselayersState = { activeBaselayer: undefined, internalBaselayers: undefined, + histogramData: undefined, }; export const CHANGE_CMAP_TYPE = 'CHANGE_CMAP'; @@ -57,11 +59,13 @@ type ChangeCmapValuesAction = { type ChangeBaselayerAction = { type: typeof CHANGE_BASELAYER; newBaselayer: InternalBaselayer | ExternalBaselayer; + histogramData?: HistogramResponse; }; type SetBaselayersAction = { type: typeof SET_BASELAYERS_STATE; internalBaselayers: InternalBaselayer[]; + histogramData: HistogramResponse; }; export type Action = @@ -78,6 +82,7 @@ export function baselayersReducer(state: BaselayersState, action: Action) { return { internalBaselayers: action.internalBaselayers, activeBaselayer: action.internalBaselayers[0], + histogramData: action.histogramData, }; } case 'CHANGE_CMAP': { @@ -87,6 +92,7 @@ export function baselayersReducer(state: BaselayersState, action: Action) { cmap: action.cmap, }; return { + ...state, internalBaselayers: state.internalBaselayers?.map((layer) => { if ( layer.layer_id === @@ -109,6 +115,10 @@ export function baselayersReducer(state: BaselayersState, action: Action) { if (assertInternalBaselayer(action.activeBaselayer)) { const { activeBaselayer, isLogScale } = action; const { vmin, vmax } = action.activeBaselayer; + + // Return early if vmin or vmax are undefined + if (vmin === undefined || vmax === undefined) return { ...state }; + const safeLogMin = safeLog(vmin); const safeLogMax = safeLog(vmax); @@ -133,6 +143,7 @@ export function baselayersReducer(state: BaselayersState, action: Action) { }; return { + ...state, internalBaselayers: state.internalBaselayers?.map((layer) => { if ( layer.layer_id === @@ -153,8 +164,10 @@ export function baselayersReducer(state: BaselayersState, action: Action) { } case 'CHANGE_ABSOLUTE_VALUE': { if (assertInternalBaselayer(action.activeBaselayer)) { - const { vmin, vmax, recommendedCmapValuesRange } = - action.activeBaselayer; + const { vmin, vmax } = action.activeBaselayer; + + // Return early if vmin or vmax are undefined + if (vmin === undefined || vmax === undefined) return { ...state }; let min = vmin; let max = vmax; @@ -163,8 +176,8 @@ export function baselayersReducer(state: BaselayersState, action: Action) { min = 0; } - if (action.isAbsoluteValue && vmax < 0) { - max = recommendedCmapValuesRange * 0.1; + if (action.isAbsoluteValue && vmax < 0 && state.histogramData) { + max = state.histogramData.vmax - state.histogramData.vmin; } const updatedActiveBaselayer = { @@ -175,6 +188,7 @@ export function baselayersReducer(state: BaselayersState, action: Action) { }; return { + ...state, internalBaselayers: state.internalBaselayers?.map((layer) => { if ( layer.layer_id === @@ -201,6 +215,7 @@ export function baselayersReducer(state: BaselayersState, action: Action) { vmax: action.vmax, }; return { + ...state, internalBaselayers: state.internalBaselayers?.map((layer) => { if ( layer.layer_id === @@ -220,12 +235,20 @@ export function baselayersReducer(state: BaselayersState, action: Action) { } } case 'CHANGE_BASELAYER': { - const { newBaselayer } = action; + const { newBaselayer, histogramData } = action; - return { - ...state, - activeBaselayer: newBaselayer, - }; + if (histogramData) { + return { + ...state, + histogramData, + activeBaselayer: newBaselayer, + }; + } else { + return { + ...state, + activeBaselayer: newBaselayer, + }; + } } default: { throw Error('Unknown action'); diff --git a/src/types/maps.ts b/src/types/maps.ts index e58a18d..c784c01 100644 --- a/src/types/maps.ts +++ b/src/types/maps.ts @@ -40,27 +40,37 @@ export type LayerResponse = { units: string; number_of_levels: number; tile_size: number; - vmin: number; - vmax: number; + /** layers' vmin/vmax are either predefined or set to 'auto' */ + vmin: number | 'auto'; + vmax: number | 'auto'; cmap: string; }; type EnhancedLayerAttributes = { mapId: string; bandId: string; - recommendedCmapValuesRange: number; isLogScale: boolean; isAbsoluteValue: boolean; }; -export type InternalBaselayer = LayerResponse & EnhancedLayerAttributes; +export type InternalBaselayer = Omit & { + /** After processing layer response, 'auto' gets set to undefined and later + * set to a value from the layer's histogram response + */ + vmin: undefined | number; + vmax: undefined | number; +} & EnhancedLayerAttributes; export type HistogramResponse = { edges: number[]; histogram: number[]; band_id: number; + vmin: number; + vmax: number; }; +export type HistogramData = Omit; + export type GraticuleDetails = { pixelWidth: number; interval: number; @@ -129,6 +139,8 @@ export type BaselayersState = { activeBaselayer?: InternalBaselayer | ExternalBaselayer; /** the internal SO layers used as baselayers */ internalBaselayers?: InternalBaselayer[]; + /** the active baselayer's histogram data */ + histogramData?: HistogramResponse; }; export type SubmapData = { diff --git a/src/utils/fetchUtils.ts b/src/utils/fetchUtils.ts index 6997ebd..df5fdca 100644 --- a/src/utils/fetchUtils.ts +++ b/src/utils/fetchUtils.ts @@ -7,6 +7,7 @@ import { SourceGroup, SourceGroupResponse, SubmapDataWithBounds, + HistogramResponse, } from '../types/maps'; import { SubmapFileExtensions } from '../configs/submapConfigs'; @@ -22,13 +23,19 @@ export async function fetchMaps() { mapGroup.maps.forEach((map) => map.bands.forEach((band) => band.layers.forEach((layer) => { + // Set to undefined if 'auto' so we can know to set this value + // with the layer's histogram response instead + const vmin = layer.vmin === 'auto' ? undefined : layer.vmin; + const vmax = layer.vmax === 'auto' ? undefined : layer.vmax; + const internalBaselayer: InternalBaselayer = { ...layer, mapId: map.map_id, bandId: band.band_id, isLogScale: false, isAbsoluteValue: false, - recommendedCmapValuesRange: layer.vmax - layer.vmin, + vmin, + vmax, }; internalBaselayers.push(internalBaselayer); }) @@ -132,3 +139,34 @@ export function getCookie(name: string): string | null { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : null; } + +// Create a cache for cmap images +const cmapCache = new Map(); + +// Get cmap image from a fetch or from the cache +export async function getCmapImage(cmap: string) { + if (cmapCache.has(cmap)) { + return cmapCache.get(cmap) as string; + } + + const image = await fetch(`${SERVICE_URL}/histograms/${cmap}.png`); + cmapCache.set(cmap, image.url); + + return image.url; +} + +// Create a cache of histogram data +const histogramCache = new Map(); + +// Get histogram data; uses a cache to only fetch the data once +export async function getHistogramData(layerId: string) { + if (histogramCache.has(layerId)) { + return histogramCache.get(layerId) as HistogramResponse; + } + + const response = await fetch(`${SERVICE_URL}/histograms/data/${layerId}`); + + const data: HistogramResponse = await response.json(); + histogramCache.set(layerId, data); + return data; +}