diff --git a/src/App.tsx b/src/App.tsx index 5102663..4c52f0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,5 @@ import { useCallback, useMemo, useState, useReducer, ChangeEvent } from 'react'; -import { - MapMetadataResponseWithClientBand, - BandWithCmapValues, - SourceList, -} from './types/maps'; +import { MapMetadataResponseWithClientBand, SourceList } from './types/maps'; import { ColorMapControls } from './components/ColorMapControls'; import { fetchProducts } from './utils/fetchUtils'; import { @@ -30,7 +26,7 @@ function App() { const [isAuthenticated, setIsAuthenticated] = useState(null); /** query the bands to use as the baselayers of the map */ - useQuery({ + useQuery({ initialData: undefined, queryKey: [isAuthenticated], queryFn: async () => { @@ -38,33 +34,17 @@ function App() { // map baselayers const mapsMetadata = await fetchProducts('maps'); - // Loop through each map's metadata and reduce the map's bands - // into a single array - const finalBands = mapsMetadata.reduce( - ( - prev: BandWithCmapValues[], - curr: MapMetadataResponseWithClientBand - ) => { - if (curr.bands.length) { - return prev.concat(curr.bands); - } else { - return prev; - } - }, - [] - ); - - // If we end up with no bands for some reason, return early - if (!finalBands.length) return; + // // If we end up with no maps for some reason, return early + if (!mapsMetadata.length) return; // Set the baselayersState with the finalBands; note that this action will also set the // activeBaselayer to be finalBands[0] dispatchBaselayersChange({ type: SET_BASELAYERS_STATE, - baselayers: finalBands, + baselayerMaps: mapsMetadata, }); - return finalBands; + return mapsMetadata; }, }); @@ -167,31 +147,29 @@ function App() { } }, [baselayersState.activeBaselayer]); - const { activeBaselayer, internalBaselayersState } = baselayersState; + const { activeBaselayer, internalBaselayerMaps } = baselayersState; return ( <> - {isAuthenticated !== null && - activeBaselayer && - internalBaselayersState && ( - - )} + {isAuthenticated !== null && activeBaselayer && internalBaselayerMaps && ( + + )} {isAuthenticated !== null && assertBand(activeBaselayer) && activeBaselayer.cmap && diff --git a/src/components/BaselayerSections.tsx b/src/components/BaselayerSections.tsx new file mode 100644 index 0000000..85cd78a --- /dev/null +++ b/src/components/BaselayerSections.tsx @@ -0,0 +1,74 @@ +import { LayerSelectorProps } from './LayerSelector'; +import { CollapsibleSection } from './CollapsibleSection'; +import { makeLayerName } from '../utils/layerUtils'; +import { EXTERNAL_BASELAYERS } from '../configs/mapSettings'; + +type BaselayerSectionsProps = { + internalBaselayerMaps: LayerSelectorProps['internalBaselayerMaps']; + activeBaselayerId: LayerSelectorProps['activeBaselayerId']; + isFlipped: LayerSelectorProps['isFlipped']; + onBaselayerChange: LayerSelectorProps['onBaselayerChange']; +}; + +export function BaselayerSections({ + internalBaselayerMaps, + activeBaselayerId, + isFlipped, + onBaselayerChange, +}: BaselayerSectionsProps) { + return ( + <> + {internalBaselayerMaps?.map((map, index) => ( + + {map.bands.map((band) => ( +
+ + onBaselayerChange( + String(band.id), + String(band.map_id), + 'layerMenu' + ) + } + /> + +
+ ))} +
+ ))} + { + + {EXTERNAL_BASELAYERS.map((bl) => ( +
+ + onBaselayerChange(bl.id, undefined, 'layerMenu') + } + disabled={bl.disabledState(isFlipped)} + /> + +
+ ))} +
+ } + + ); +} diff --git a/src/components/CollapsibleSection.tsx b/src/components/CollapsibleSection.tsx new file mode 100644 index 0000000..0f89857 --- /dev/null +++ b/src/components/CollapsibleSection.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; + +type Props = { + summary: ReactNode; + children: ReactNode; + defaultOpen: boolean; +}; + +export function CollapsibleSection({ summary, defaultOpen, children }: Props) { + return ( +
+ {summary} + {children} +
+ ); +} diff --git a/src/components/LayerSelector.tsx b/src/components/LayerSelector.tsx index a492bc8..ca10d00 100644 --- a/src/components/LayerSelector.tsx +++ b/src/components/LayerSelector.tsx @@ -1,18 +1,17 @@ import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { BandWithCmapValues, SourceList } from '../types/maps'; -import { makeLayerName } from '../utils/layerUtils'; +import { MapMetadataResponseWithClientBand, SourceList } from '../types/maps'; import { LayersIcon } from './icons/LayersIcon'; import './styles/layer-selector.css'; import { MapProps } from './OpenLayersMap'; -import { EXTERNAL_BASELAYERS } from '../configs/mapSettings'; import { BaselayerHistoryNavigation, BaselayerHistoryNavigationProps, } from './BaselayerHistoryNavigation'; import { LockClosedIcon } from './icons/LockClosedIcon'; import { LockOpenIcon } from './icons/LockOpenIcon'; +import { BaselayerSections } from './BaselayerSections'; -interface Props +export interface LayerSelectorProps extends Omit< MapProps & BaselayerHistoryNavigationProps, | 'baselayersState' @@ -24,17 +23,18 @@ interface Props > { onBaselayerChange: ( selectedBaselayerId: string, + selectedMapId: string | undefined, context: 'layerMenu' | 'goBack' | 'goForward', flipped?: boolean ) => void; activeBaselayerId?: number | string; sourceLists: SourceList[]; isFlipped: boolean; - internalBaselayers: BandWithCmapValues[] | undefined; + internalBaselayerMaps: MapMetadataResponseWithClientBand[] | undefined; } export function LayerSelector({ - internalBaselayers, + internalBaselayerMaps, onBaselayerChange, activeBaselayerId, sourceLists, @@ -48,7 +48,7 @@ export function LayerSelector({ disableGoForward, goBack, goForward, -}: Props) { +}: LayerSelectorProps) { const menuRef = useRef(null); const [lockMenu, setLockMenu] = useState(false); const previousLockMenuHandlerRef = useRef<(e: KeyboardEvent) => void>(null); @@ -133,41 +133,12 @@ export function LayerSelector({
Baselayers - {internalBaselayers?.map((band) => ( -
- onBaselayerChange(e.target.value, 'layerMenu')} - /> - -
- ))} - {EXTERNAL_BASELAYERS.map((bl) => ( -
- onBaselayerChange(e.target.value, 'layerMenu')} - disabled={bl.disabledState(isFlipped)} - /> - -
- ))} +
{sourceLists.length ? (
diff --git a/src/components/OpenLayersMap.tsx b/src/components/OpenLayersMap.tsx index fff7747..e23579c 100644 --- a/src/components/OpenLayersMap.tsx +++ b/src/components/OpenLayersMap.tsx @@ -17,7 +17,14 @@ import VectorSource from 'ol/source/Vector'; import { Point } from 'ol/geom'; import { Circle as CircleStyle, Style, Fill, Stroke } from 'ol/style'; import 'ol/ol.css'; -import { BaselayersState, Box, SourceList, SubmapData } from '../types/maps'; +import { + BandWithCmapValues, + BaselayersState, + Box, + ExternalBaselayer, + SourceList, + SubmapData, +} from '../types/maps'; import { DEFAULT_INTERNAL_MAP_SETTINGS, EXTERNAL_BASELAYERS, @@ -88,13 +95,13 @@ export function OpenLayersMap({ const [flipTiles, setFlipTiles] = useState(true); const [backHistoryStack, setBackHistoryStack] = useState< - { id: string; flipped: boolean }[] + { id: string; mapId: string | undefined; flipped: boolean }[] >([]); const [forwardHistoryStack, setForwardHistoryStack] = useState< - { id: string; flipped: boolean }[] + { id: string; mapId: string | undefined; flipped: boolean }[] >([]); - const { activeBaselayer, internalBaselayersState } = baselayersState; + const { activeBaselayer, internalBaselayerMaps } = baselayersState; /** * Handler fires when user changes map layers. If the units of the new @@ -106,6 +113,7 @@ export function OpenLayersMap({ const onBaselayerChange = useCallback( ( selectedBaselayerId: string, + selectedBaselayerMapId: string | undefined, context: 'layerMenu' | 'goBack' | 'goForward', flipped?: boolean ) => { @@ -113,11 +121,24 @@ export function OpenLayersMap({ const { activeBaselayer } = baselayersState; - const newActiveBaselayer = isExternalBaselayer - ? EXTERNAL_BASELAYERS.find((b) => b.id === selectedBaselayerId) - : baselayersState.internalBaselayersState?.find( - (b) => b.id === Number(selectedBaselayerId) - ); + let newActiveBaselayer: + | ExternalBaselayer + | BandWithCmapValues + | undefined = undefined; + + if (isExternalBaselayer) { + newActiveBaselayer = EXTERNAL_BASELAYERS.find( + (b) => b.id === selectedBaselayerId + ); + } else { + const map = baselayersState.internalBaselayerMaps?.find( + (map) => map.id === Number(selectedBaselayerMapId) + ); + newActiveBaselayer = map?.bands.find( + (b) => b.id === Number(selectedBaselayerId) + ); + } + if (!newActiveBaselayer) return; if (context === 'goBack') { @@ -125,6 +146,10 @@ export function OpenLayersMap({ setForwardHistoryStack((prev) => [...prev].concat({ id: String(activeBaselayer?.id), + mapId: + activeBaselayer && 'map_id' in activeBaselayer + ? String(activeBaselayer.map_id) + : undefined, flipped: flipTiles, }) ); @@ -132,6 +157,10 @@ export function OpenLayersMap({ setBackHistoryStack((prev) => [...prev].concat({ id: String(activeBaselayer?.id), + mapId: + activeBaselayer && 'map_id' in activeBaselayer + ? String(activeBaselayer.map_id) + : undefined, flipped: flipTiles, }) ); @@ -140,6 +169,10 @@ export function OpenLayersMap({ setBackHistoryStack((prev) => [...prev].concat({ id: String(activeBaselayer?.id), + mapId: + activeBaselayer && 'map_id' in activeBaselayer + ? String(activeBaselayer.map_id) + : undefined, flipped: flipTiles, }) ); @@ -156,7 +189,7 @@ export function OpenLayersMap({ }); }, [ - baselayersState.internalBaselayersState, + baselayersState.internalBaselayerMaps, baselayersState.activeBaselayer, backHistoryStack, flipTiles, @@ -166,37 +199,55 @@ export function OpenLayersMap({ const goBack = useCallback(() => { const baselayer = backHistoryStack[backHistoryStack.length - 1]; - onBaselayerChange(baselayer.id, 'goBack', baselayer.flipped); + onBaselayerChange( + baselayer.id, + baselayer.mapId, + 'goBack', + baselayer.flipped + ); }, [onBaselayerChange, backHistoryStack]); const goForward = useCallback(() => { const baselayer = forwardHistoryStack[forwardHistoryStack.length - 1]; - onBaselayerChange(baselayer.id, 'goForward', baselayer.flipped); + onBaselayerChange( + baselayer.id, + baselayer.mapId, + 'goForward', + baselayer.flipped + ); }, [onBaselayerChange, forwardHistoryStack]); const tileLayers = useMemo(() => { - return internalBaselayersState?.map((band) => { - return new TileLayer({ - properties: { id: 'baselayer-' + band.id }, - source: new XYZ({ - url: `${SERVICE_URL}/maps/${band.map_id}/${band.id}/{z}/{-y}/{x}/tile.png?cmap=${band.cmap}&vmin=${band.cmapValues.min}&vmax=${band.cmapValues.max}&flip=${flipTiles}`, - tileGrid: new TileGrid({ - extent: [-180, -90, 180, 90], - origin: [-180, 90], - tileSize: band.tile_size, - resolutions: getBaselayerResolutions( - 180, - band.tile_size, - band.levels - 1 - ), - }), - interpolate: false, - projection: 'EPSG:4326', - tilePixelRatio: band.tile_size / 256, - }), + const tempTileLayers: TileLayer[] = []; + + internalBaselayerMaps?.forEach((map) => { + map.bands.forEach((band) => { + tempTileLayers.push( + new TileLayer({ + properties: { id: 'baselayer-' + band.id }, + source: new XYZ({ + url: `${SERVICE_URL}/maps/${band.map_id}/${band.id}/{z}/{-y}/{x}/tile.png?cmap=${band.cmap}&vmin=${band.cmapValues.min}&vmax=${band.cmapValues.max}&flip=${flipTiles}`, + tileGrid: new TileGrid({ + extent: [-180, -90, 180, 90], + origin: [-180, 90], + tileSize: band.tile_size, + resolutions: getBaselayerResolutions( + 180, + band.tile_size, + band.levels - 1 + ), + }), + interpolate: false, + projection: 'EPSG:4326', + tilePixelRatio: band.tile_size / 256, + }), + }) + ); }); }); - }, [internalBaselayersState, flipTiles]); + + return tempTileLayers; + }, [internalBaselayerMaps, flipTiles]); const externalTileLayers = useMemo(() => { return EXTERNAL_BASELAYERS.map((b) => { @@ -522,7 +573,7 @@ export function OpenLayersMap({ flipped={flipTiles} /> input { + margin: 0; + font-size: 0.5; } .layers-icon { diff --git a/src/configs/mapSettings.ts b/src/configs/mapSettings.ts index c723acb..a07dddf 100644 --- a/src/configs/mapSettings.ts +++ b/src/configs/mapSettings.ts @@ -22,29 +22,29 @@ export const DEFAULT_INTERNAL_MAP_SETTINGS = { export const EXTERNAL_BASELAYERS: ExternalBaselayer[] = [ { - id: 'external-unwise-neo4', - name: 'Legacy Survey | unWISE neo4', + id: 'external-unwise-neo6', + name: 'Legacy Survey | unWISE neo6', projection: 'EPSG:3857', - url: 'https://imagine.legacysurvey.org/static/tiles/unwise-neo4/1/{z}/{x}/{y}.jpg', + url: 'https://s3.us-west-2.amazonaws.com/unwise-neo6.legacysurvey.org/{z}/{x}/{y}.jpg', extent: transformExtent( [-180, -MERCATOR_MAX_LAT, 180, MERCATOR_MAX_LAT], 'EPSG:4326', 'EPSG:3857' ), - maxZoom: 8, + maxZoom: 10, disabledState: (isFlipped: boolean) => !isFlipped, }, { - id: 'external-unwise-neo6', - name: 'Legacy Survey | unWISE neo6', + id: 'external-unwise-neo4', + name: 'Legacy Survey | unWISE neo4', projection: 'EPSG:3857', - url: 'https://s3.us-west-2.amazonaws.com/unwise-neo6.legacysurvey.org/{z}/{x}/{y}.jpg', + url: 'https://imagine.legacysurvey.org/static/tiles/unwise-neo4/1/{z}/{x}/{y}.jpg', extent: transformExtent( [-180, -MERCATOR_MAX_LAT, 180, MERCATOR_MAX_LAT], 'EPSG:4326', 'EPSG:3857' ), - maxZoom: 10, + maxZoom: 8, disabledState: (isFlipped: boolean) => !isFlipped, }, ]; diff --git a/src/index.css b/src/index.css index 642f79e..d26a5b5 100644 --- a/src/index.css +++ b/src/index.css @@ -151,3 +151,7 @@ body { background-color: #921925 !important; color: white !important; } + +summary { + cursor: default; +} diff --git a/src/reducers/baselayersReducer.ts b/src/reducers/baselayersReducer.ts index 916f911..863acd6 100644 --- a/src/reducers/baselayersReducer.ts +++ b/src/reducers/baselayersReducer.ts @@ -2,6 +2,7 @@ import { BandWithCmapValues, BaselayersState, ExternalBaselayer, + MapMetadataResponseWithClientBand, } from '../types/maps'; export function assertExternalBaselayer( @@ -18,7 +19,7 @@ export function assertBand( export const initialBaselayersState: BaselayersState = { activeBaselayer: undefined, - internalBaselayersState: undefined, + internalBaselayerMaps: undefined, }; export const CHANGE_CMAP_TYPE = 'CHANGE_CMAP'; @@ -48,7 +49,7 @@ type ChangeBaselayerAction = { type SetBaselayersAction = { type: typeof SET_BASELAYERS_STATE; - baselayers: BandWithCmapValues[]; + baselayerMaps: MapMetadataResponseWithClientBand[]; }; export type Action = @@ -61,8 +62,8 @@ export function baselayersReducer(state: BaselayersState, action: Action) { switch (action.type) { case 'SET_BASELAYERS_STATE': { return { - internalBaselayersState: action.baselayers, - activeBaselayer: action.baselayers[0], + internalBaselayerMaps: action.baselayerMaps, + activeBaselayer: action.baselayerMaps[0].bands[0], }; } case 'CHANGE_CMAP': { @@ -72,9 +73,21 @@ export function baselayersReducer(state: BaselayersState, action: Action) { cmap: action.cmap, }; return { - internalBaselayersState: state.internalBaselayersState?.map((b) => - b.id === action.activeBaselayer.id ? activeBaselayer : b - ), + internalBaselayerMaps: state.internalBaselayerMaps?.map((map) => { + if ( + 'map_id' in action.activeBaselayer && + map.id === action.activeBaselayer.map_id + ) { + return { + ...map, + bands: map.bands.map((b) => + b.id === action.activeBaselayer.id ? activeBaselayer : b + ), + }; + } else { + return map; + } + }), activeBaselayer, }; } else { @@ -95,9 +108,21 @@ export function baselayersReducer(state: BaselayersState, action: Action) { }, }; return { - internalBaselayersState: state.internalBaselayersState?.map((b) => - b.id === action.activeBaselayer.id ? activeBaselayer : b - ), + internalBaselayerMaps: state.internalBaselayerMaps?.map((map) => { + if ( + 'map_id' in action.activeBaselayer && + map.id === action.activeBaselayer.map_id + ) { + return { + ...map, + bands: map.bands.map((b) => + b.id === action.activeBaselayer.id ? activeBaselayer : b + ), + }; + } else { + return map; + } + }), activeBaselayer, }; } else { diff --git a/src/types/maps.ts b/src/types/maps.ts index dc2134b..05dc539 100644 --- a/src/types/maps.ts +++ b/src/types/maps.ts @@ -121,8 +121,8 @@ export type ExternalBaselayer = { export type BaselayersState = { /** the active baselayer selected in the map's legend */ activeBaselayer?: BandWithCmapValues | ExternalBaselayer; - /** the bands used as internal baselayers */ - internalBaselayersState?: BandWithCmapValues[]; + /** the internal maps whose bands are used as internal baselayers */ + internalBaselayerMaps?: MapMetadataResponseWithClientBand[]; }; export type SubmapData = {