diff --git a/app/scripts/components/common/mapbox/index.tsx b/app/scripts/components/common/mapbox/index.tsx index eaf22a64d..23e692f5d 100644 --- a/app/scripts/components/common/mapbox/index.tsx +++ b/app/scripts/components/common/mapbox/index.tsx @@ -419,9 +419,11 @@ function MapboxMapComponent( id={`base-${baseLayerResolvedData.id}`} stacCol={baseLayerResolvedData.stacCol} mapInstance={mapRef.current} + isPositionSet={!!initialPosition} date={date} sourceParams={baseLayerResolvedData.sourceParams} zoomExtent={baseLayerResolvedData.zoomExtent} + bounds={baseLayerResolvedData.bounds} onStatusChange={onBaseLayerStatusChange} /> )} @@ -471,6 +473,7 @@ function MapboxMapComponent( date={compareToDate ?? undefined} sourceParams={compareLayerResolvedData.sourceParams} zoomExtent={compareLayerResolvedData.zoomExtent} + bounds={compareLayerResolvedData.bounds} onStatusChange={onCompareLayerStatusChange} /> )} diff --git a/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx b/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx index a6db13516..8826a3683 100644 --- a/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx +++ b/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx @@ -15,10 +15,11 @@ import { featureCollection, point } from '@turf/helpers'; import { useMapStyle } from './styles'; import { - checkFitBoundsFromLayer, + FIT_BOUNDS_PADDING, getFilterPayload, getMergedBBox, requestQuickCache, + useFitBbox, useLayerInteraction } from './utils'; import { useCustomMarker } from './custom-marker'; @@ -34,8 +35,6 @@ import { // Whether or not to print the request logs. const LOG = true; -const FIT_BOUNDS_PADDING = 32; - export interface MapLayerRasterTimeseriesProps { id: string; stacCol: string; @@ -43,9 +42,11 @@ export interface MapLayerRasterTimeseriesProps { mapInstance: MapboxMap; sourceParams?: Record; zoomExtent?: number[]; + bounds?: number[]; onStatusChange?: (result: { status: ActionStatus; id: string }) => void; isHidden?: boolean; idSuffix?: string; + isPositionSet?: boolean; } export interface StacFeature { @@ -72,9 +73,11 @@ export function MapLayerRasterTimeseries(props: MapLayerRasterTimeseriesProps) { mapInstance, sourceParams, zoomExtent, + bounds, onStatusChange, isHidden, - idSuffix = '' + idSuffix = '', + isPositionSet } = props; const theme = useTheme(); @@ -466,14 +469,11 @@ export function MapLayerRasterTimeseries(props: MapLayerRasterTimeseriesProps) { // // FitBounds when needed // - useEffect(() => { - if (!stacCollection.length) return; - const layerBounds = getMergedBBox(stacCollection); - - if (checkFitBoundsFromLayer(layerBounds, mapInstance)) { - mapInstance.fitBounds(layerBounds, { padding: FIT_BOUNDS_PADDING }); - } - }, [mapInstance, stacCol, stacCollection]); + const layerBounds = useMemo( + () => (stacCollection.length ? getMergedBBox(stacCollection) : undefined), + [stacCollection] + ); + useFitBbox(mapInstance, !!isPositionSet, bounds, layerBounds); return null; } diff --git a/app/scripts/components/common/mapbox/layers/utils.ts b/app/scripts/components/common/mapbox/layers/utils.ts index 91f46f731..d7300b357 100644 --- a/app/scripts/components/common/mapbox/layers/utils.ts +++ b/app/scripts/components/common/mapbox/layers/utils.ts @@ -133,7 +133,7 @@ export const getCompareLayerData = ( type: otherLayer.type, name: otherLayer.name, description: otherLayer.description, - legend: otherLayer.legend, + legend: otherLayer.legend, stacCol: otherLayer.stacCol, zoomExtent: zoomExtent ?? otherLayer.zoomExtent, sourceParams: defaultsDeep({}, sourceParams, otherLayer.sourceParams), @@ -370,6 +370,8 @@ export function getMergedBBox(features: StacFeature[]) { ) as [number, number, number, number]; } +export const FIT_BOUNDS_PADDING = 32; + export function checkFitBoundsFromLayer( layerBounds?: [number, number, number, number], mapInstance?: MapboxMap @@ -429,3 +431,35 @@ export function useLayerInteraction({ }; }, [layerId, mapInstance, onClick]); } + +type OptionalBbox = number[] | undefined | null; + +/** + * Centers on the given bounds if the current position is not within the bounds, + * and there's no user defined position (via user initiated map movement). Gives + * preference to the layer defined bounds over the STAC collection bounds. + * + * @param mapInstance Mapbox instance + * @param isUserPositionSet Whether the user has set a position + * @param initialBbox Bounding box from the layer + * @param stacBbox Bounds from the STAC collection + */ +export function useFitBbox( + mapInstance: MapboxMap, + isUserPositionSet: boolean, + initialBbox: OptionalBbox, + stacBbox: OptionalBbox +) { + useEffect(() => { + if (isUserPositionSet) return; + + // Prefer layer defined bounds to STAC collection bounds. + const bounds = (initialBbox ?? stacBbox) as + | [number, number, number, number] + | undefined; + + if (bounds?.length && checkFitBoundsFromLayer(bounds, mapInstance)) { + mapInstance.fitBounds(bounds, { padding: FIT_BOUNDS_PADDING }); + } + }, [mapInstance, isUserPositionSet, initialBbox, stacBbox]); +} diff --git a/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx b/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx index bf200d927..3300a3d41 100644 --- a/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx +++ b/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx @@ -12,7 +12,7 @@ import { Feature } from 'geojson'; import { endOfDay, startOfDay } from 'date-fns'; import centroid from '@turf/centroid'; -import { requestQuickCache, useLayerInteraction } from './utils'; +import { requestQuickCache, useFitBbox, useLayerInteraction } from './utils'; import { useMapStyle } from './styles'; import { useCustomMarker } from './custom-marker'; @@ -26,9 +26,11 @@ export interface MapLayerVectorTimeseriesProps { mapInstance: MapboxMap; sourceParams?: Record; zoomExtent?: number[]; + bounds?: number[]; onStatusChange?: (result: { status: ActionStatus; id: string }) => void; isHidden?: boolean; idSuffix?: string; + isPositionSet?: boolean; } export function MapLayerVectorTimeseries(props: MapLayerVectorTimeseriesProps) { @@ -39,14 +41,18 @@ export function MapLayerVectorTimeseries(props: MapLayerVectorTimeseriesProps) { mapInstance, sourceParams, zoomExtent, + bounds, onStatusChange, isHidden, - idSuffix = '' + idSuffix = '', + isPositionSet } = props; const theme = useTheme(); const { updateStyle } = useMapStyle(); const [featuresApiEndpoint, setFeaturesApiEndpoint] = useState(''); + const [featuresBbox, setFeaturesBbox] = + useState<[number, number, number, number]>(); const [minZoom, maxZoom] = zoomExtent ?? [0, 20]; @@ -67,7 +73,19 @@ export function MapLayerVectorTimeseries(props: MapLayerVectorTimeseriesProps) { controller }); - setFeaturesApiEndpoint(data.links.find((l) => l.rel === 'external').href); + const endpoint = data.links.find((l) => l.rel === 'external').href; + setFeaturesApiEndpoint(endpoint); + + const featuresData = await requestQuickCache({ + url: endpoint, + method: 'GET', + controller + }); + + if (featuresData.extent.spatial.bbox) { + setFeaturesBbox(featuresData.extent.spatial.bbox[0]); + } + onStatusChange?.({ status: S_SUCCEEDED, id }); } catch (error) { if (!controller.signal.aborted) { @@ -85,7 +103,6 @@ export function MapLayerVectorTimeseries(props: MapLayerVectorTimeseriesProps) { }; }, [mapInstance, id, stacCol, date, onStatusChange]); - const markerLayout = useCustomMarker(mapInstance); // @@ -192,7 +209,7 @@ export function MapLayerVectorTimeseries(props: MapLayerVectorTimeseriesProps) { 'source-layer': 'default', layout: { ...(markerLayout as any), - visibility: isHidden ? 'none' : 'visible', + visibility: isHidden ? 'none' : 'visible' }, paint: { 'icon-color': theme.color?.infographicB, @@ -266,5 +283,10 @@ export function MapLayerVectorTimeseries(props: MapLayerVectorTimeseriesProps) { onClick: onPointsClick }); + // + // FitBounds when needed + // + useFitBbox(mapInstance, !!isPositionSet, bounds, featuresBbox); + return null; } diff --git a/app/scripts/components/datasets/s-explore/index.tsx b/app/scripts/components/datasets/s-explore/index.tsx index 9b63e8c29..7f7450ae1 100644 --- a/app/scripts/components/datasets/s-explore/index.tsx +++ b/app/scripts/components/datasets/s-explore/index.tsx @@ -215,6 +215,7 @@ function DatasetsExplore() { useEffect(() => { setPanelRevealed(!isMediumDown); }, [isMediumDown]); + // When the panel changes resize the map after a the animation finishes. useEffect(() => { const id = setTimeout( @@ -568,7 +569,13 @@ function DatasetsExplore() { compareDate={selectedCompareDatetime ?? undefined} isComparing={isComparing} initialPosition={mapPosition ?? undefined} - onPositionChange={setMapPosition} + onPositionChange={(v) => { + // Only store the map position if the change was initiated by + // the user. + if (v.userInitiated) { + setMapPosition(v); + } + }} projection={mapProjection ?? projectionDefault} onProjectionChange={setMapProjection} /> diff --git a/docs/content/frontmatter/layer.md b/docs/content/frontmatter/layer.md index 2fd07ef98..5f84556de 100644 --- a/docs/content/frontmatter/layer.md +++ b/docs/content/frontmatter/layer.md @@ -16,6 +16,7 @@ initialDatetime: 'oldest' | 'newest' | Date(YYYY-MM-DD) = 'newest' description: string projection: Projection zoomExtent: [int, int] | null | fn(bag) +bounds: [int, int, int, int] | null | fn(bag) sourceParams: [key]: value | fn(bag) compare: Compare @@ -67,6 +68,20 @@ These values may vary greatly depending on the layer being added but some may be `string` The colormap to use for the layer. One of https://cogeotiff.github.io/rio-tiler/colormap/#default-rio-tilers-colormaps +**bounds** +`[int, int, int, int] | fn(bag)` +Initial bounds for the map. This is useful for adjusting the initial view on datasets for which the STAC bounds are not appropriate. + +This property should be an array with 4 numbers, representing the minimum and maximum longitude and latitude values, in the following order: [minLongitude, minLatitude, maxLongitude, maxLatitude]. +Example (world bounds) +```yml +bounds: [-180, -90, 180, 90] +``` + +Note on bounds and dataset layer switching: +The exploration map will always prioritize the position set in the url. This is so that the user can share a link to a specific location. However, upon load the map will check if the position set in the url is within or overlapping the bounds of the dataset layer. If it is not, the map will switch to the dataset layer bounds avoiding showing an empty map when the user shares a link to a location that is not within the dataset layer bounds. +If there are no bounds set in the dataset configuration, the bbox from the STAC catalog will be used if available, otherwise it will default to the world bounds. + ### Projection **projection** diff --git a/mock/datasets/fire.data.mdx b/mock/datasets/fire.data.mdx index b7fe01504..8de00c53b 100644 --- a/mock/datasets/fire.data.mdx +++ b/mock/datasets/fire.data.mdx @@ -23,17 +23,9 @@ taxonomy: values: - COx layers: - - id: eis_fire_fireline - stacCol: eis_fire_fireline - name: Fire - type: vector - description: eis_fire_fireline - zoomExtent: - - 5 - - 20 - id: eis_fire_perimeter stacCol: eis_fire_perimeter - name: Fire Perimeter + name: Fire type: vector description: eis_fire_perimeter zoomExtent: diff --git a/mock/datasets/no2.data.mdx b/mock/datasets/no2.data.mdx index b64ea8519..b4313ab58 100644 --- a/mock/datasets/no2.data.mdx +++ b/mock/datasets/no2.data.mdx @@ -37,8 +37,9 @@ taxonomy: layers: - id: no2-monthly stacCol: no2-monthly - name: No2 + name: No2 PT type: raster + bounds: [-10, 36, -5, 42] description: Levels in 10¹⁵ molecules cm⁻². Darker colors indicate higher nitrogen dioxide (NO₂) levels associated and more activity. Lighter colors indicate lower levels of NO₂ and less activity. zoomExtent: - 0 @@ -73,7 +74,8 @@ layers: - "#050308" - id: no2-monthly-2 stacCol: no2-monthly - name: No2 + name: No2 US + bounds: [-124, 29, -65, 49] type: raster description: Levels in 10¹⁵ molecules cm⁻². Darker colors indicate higher nitrogen dioxide (NO₂) levels associated and more activity. Lighter colors indicate lower levels of NO₂ and less activity. zoomExtent: diff --git a/parcel-resolver-veda/index.d.ts b/parcel-resolver-veda/index.d.ts index 420fe6b71..3578f925e 100644 --- a/parcel-resolver-veda/index.d.ts +++ b/parcel-resolver-veda/index.d.ts @@ -26,6 +26,7 @@ declare module 'veda' { interface DatasetLayerCommonProps { zoomExtent?: number[]; + bounds?: number[]; sourceParams?: Record; }