From 73a0d60d52b3e9119f4d299e9e2f5de7989d3b0b Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 27 Mar 2024 18:15:26 +0900 Subject: [PATCH 1/7] Start aoi zoom --- .../map/analysis-message-control.tsx | 78 ++++++++++++++----- .../components/map/tour-control.tsx | 1 - 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index fe2697fa4..1cda917cd 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -1,23 +1,39 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useAtomValue } from 'jotai'; import styled, { css } from 'styled-components'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { Button, createButtonStyles } from '@devseed-ui/button'; +import bbox from '@turf/bbox'; import { CollecticonChartLine, CollecticonCircleInformation } from '@devseed-ui/collecticons'; - +import useMaps from '$components/common/map/hooks/use-maps'; import { timelineDatasetsAtom } from '../../atoms/datasets'; import { selectedIntervalAtom } from '../../atoms/dates'; import useAois from '$components/common/map/controls/hooks/use-aois'; -import { calcFeatCollArea } from '$components/common/aoi/utils'; +import { calcFeatCollArea, boundsFromFeature } from '$components/common/aoi/utils'; import { formatDateRange } from '$utils/date'; import { useAnalysisController } from '$components/exploration/hooks/use-analysis-data-request'; import useThemedControl from '$components/common/map/controls/hooks/use-themed-control'; import { AoIFeature } from '$components/common/map/types'; import { ShortcutCode } from '$styles/shortcut-code'; +export function getZoomFromBbox(bbox: [number, number, number, number]): number { + const latMax = Math.max(bbox[3], bbox[1]); + const lngMax = Math.max(bbox[2], bbox[0]); + const latMin = Math.min(bbox[3], bbox[1]); + const lngMin = Math.min(bbox[2], bbox[0]); + const maxDiff = Math.max(latMax - latMin, lngMax - lngMin); + if (maxDiff < 360 / Math.pow(2, 20)) { + return 21; +} else { + const zoomLevel = Math.floor(-1*( (Math.log(maxDiff)/Math.log(2)) - (Math.log(360)/Math.log(2)))); + if (zoomLevel < 1) return 1; + else return zoomLevel; + } +} + const AnalysisMessageWrapper = styled.div.attrs({ 'data-tour': 'analysis-message' @@ -81,7 +97,7 @@ const MessageControls = styled.div` gap: ${glsp(0.5)}; `; -export function AnalysisMessage() { +export function AnalysisMessage({ maps }) { const { isObsolete, setObsolete, isAnalyzing } = useAnalysisController(); const datasets = useAtomValue(timelineDatasetsAtom); @@ -94,7 +110,6 @@ export function AnalysisMessage() { formatDateRange(selectedInterval.start, selectedInterval.end); const selectedFeatures = features.filter((f) => f.selected); - useEffect(() => { // Set the analysis as obsolete when the selected features change. setObsolete(); @@ -105,6 +120,7 @@ export function AnalysisMessage() { @@ -123,7 +140,8 @@ export function AnalysisMessage() { } export function AnalysisMessageControl() { - useThemedControl(() => , { position: 'top-left' }); + const maps = useMaps() + useThemedControl(() => , { position: 'top-left' }); return null; } @@ -139,7 +157,7 @@ interface MessagesProps { function MessagesWhileAnalyzing( props: MessagesProps & { isObsolete: boolean } ) { - const { isObsolete, features, selectedFeatures, datasetIds, dateLabel } = + const { isObsolete, features, selectedFeatures, datasetIds, dateLabel, mapInstance } = props; const area = calcFeatCollArea({ @@ -187,7 +205,7 @@ function MessagesWhileAnalyzing( - + @@ -233,7 +251,7 @@ function MessagesWhileAnalyzing( } function MessagesWhileNotAnalyzing(props: MessagesProps) { - const { features, selectedFeatures, datasetIds, dateLabel } = props; + const { features, selectedFeatures, datasetIds, dateLabel, mapInstance } = props; if (selectedFeatures.length) { // Not analyzing, but there are selected features. @@ -262,7 +280,7 @@ function MessagesWhileNotAnalyzing(props: MessagesProps) { - + ); @@ -328,16 +346,28 @@ const Btn = styled(Button)` `; function ButtonObsolete(props: { datasetIds: string[] }) { - const { datasetIds } = props; + const { datasetIds, features, mapInstance } = props; const { runAnalysis } = useAnalysisController(); + const handleClick = useCallback(() => { + runAnalysis(datasetIds); + const bboxToFit = bbox({ + type: 'FeatureCollection', + features + }) + + const zoom = bboxToFit? getZoomFromBbox(bboxToFit): 14; + mapInstance?.flyTo({ + center:[ (bboxToFit[2] + bboxToFit[0])/2, (bboxToFit[3] + bboxToFit[1])/2], + zoom + }); + },[datasetIds, mapInstance, features]) + return ( { - runAnalysis(datasetIds); - }} + onClick={handleClick} > Apply changes @@ -360,16 +390,28 @@ function ButtonExit() { } function ButtonAnalyze(props: { datasetIds: string[] }) { - const { datasetIds } = props; + const { datasetIds, mapInstance, features } = props; const { runAnalysis } = useAnalysisController(); + const handleClick = useCallback(() => { + runAnalysis(datasetIds); + const bboxToFit = bbox({ + type: 'FeatureCollection', + features + }) + + const zoom = bboxToFit? getZoomFromBbox(bboxToFit): 14; + mapInstance?.flyTo({ + center:[ (bboxToFit[2] + bboxToFit[0])/2, (bboxToFit[3] + bboxToFit[1])/2], + zoom + }); + },[datasetIds, mapInstance, features]) + return ( { - runAnalysis(datasetIds); - }} + onClick={handleClick} > Run analysis diff --git a/app/scripts/components/exploration/components/map/tour-control.tsx b/app/scripts/components/exploration/components/map/tour-control.tsx index 89d96a627..9cad6ae9a 100644 --- a/app/scripts/components/exploration/components/map/tour-control.tsx +++ b/app/scripts/components/exploration/components/map/tour-control.tsx @@ -27,7 +27,6 @@ export function ShowTourControl() { const { setIsOpen, setCurrentStep, setSteps } = useTour(); const datasets = useAtomValue(timelineDatasetsAtom); const disabled = datasets.length === 0; - const reopenTour = useCallback(() => { setCurrentStep(0); setSteps?.(introTourSteps); From edc3c25356ee714400fb37e709420a36a3a03f59 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Fri, 29 Mar 2024 11:05:38 +0900 Subject: [PATCH 2/7] Add AOI zoom functionality --- .../common/map/controls/geocoder.tsx | 18 +--- app/scripts/components/common/map/utils.ts | 17 ++++ app/scripts/components/common/mapbox/map.tsx | 2 +- .../map/analysis-message-control.tsx | 86 ++++++++----------- 4 files changed, 54 insertions(+), 69 deletions(-) diff --git a/app/scripts/components/common/map/controls/geocoder.tsx b/app/scripts/components/common/map/controls/geocoder.tsx index 603c0931b..30b343030 100644 --- a/app/scripts/components/common/map/controls/geocoder.tsx +++ b/app/scripts/components/common/map/controls/geocoder.tsx @@ -1,25 +1,11 @@ import { useCallback } from 'react'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; -import { useControl } from 'react-map-gl'; -export function getZoomFromBbox(bbox: [number, number, number, number]): number { - const latMax = Math.max(bbox[3], bbox[1]); - const lngMax = Math.max(bbox[2], bbox[0]); - const latMin = Math.min(bbox[3], bbox[1]); - const lngMin = Math.min(bbox[2], bbox[0]); - const maxDiff = Math.max(latMax - latMin, lngMax - lngMin); - if (maxDiff < 360 / Math.pow(2, 20)) { - return 21; -} else { - const zoomLevel = Math.floor(-1*( (Math.log(maxDiff)/Math.log(2)) - (Math.log(360)/Math.log(2)))); - if (zoomLevel < 1) return 1; - else return zoomLevel; - } -} +import { useControl } from 'react-map-gl'; +import { getZoomFromBbox } from '$components/common/map/utils'; export default function GeocoderControl() { - const handleGeocoderResult = useCallback((map, geocoder) => ({ result }) => { geocoder.clear(); geocoder._inputEl.blur(); diff --git a/app/scripts/components/common/map/utils.ts b/app/scripts/components/common/map/utils.ts index 6d5a1cff2..1a542e7af 100644 --- a/app/scripts/components/common/map/utils.ts +++ b/app/scripts/components/common/map/utils.ts @@ -3,6 +3,7 @@ import { Map as MapboxMap } from 'mapbox-gl'; import { MapRef } from 'react-map-gl'; import { endOfDay, startOfDay } from 'date-fns'; import { Feature, MultiPolygon, Polygon } from 'geojson'; +import { BBox } from "@turf/helpers"; import { DatasetDatumFn, DatasetDatumFnResolverBag, @@ -232,3 +233,19 @@ export function multiPolygonToPolygons(feature: Feature) { return polygons; } + +export function getZoomFromBbox(bbox: BBox): number { + const latMax = Math.max(bbox[3], bbox[1]); + const lngMax = Math.max(bbox[2], bbox[0]); + const latMin = Math.min(bbox[3], bbox[1]); + const lngMin = Math.min(bbox[2], bbox[0]); + const maxDiff = Math.max(latMax - latMin, lngMax - lngMin); + if (maxDiff < 360 / Math.pow(2, 20)) { + return 21; +} else { + const zoomLevel = Math.floor(-1*( (Math.log(maxDiff)/Math.log(2)) - (Math.log(360)/Math.log(2)))); + if (zoomLevel < 1) return 1; + else return zoomLevel; + } +} + diff --git a/app/scripts/components/common/mapbox/map.tsx b/app/scripts/components/common/mapbox/map.tsx index a6d508f33..435afcbe5 100644 --- a/app/scripts/components/common/mapbox/map.tsx +++ b/app/scripts/components/common/mapbox/map.tsx @@ -28,7 +28,7 @@ import MapCoords from './map-coords'; import { useMapStyle } from './layers/styles'; import { BasemapId, Option } from './map-options/basemaps'; -import { getZoomFromBbox } from '$components/common/map/controls/geocoder'; +import { getZoomFromBbox } from '$components/common/map/utils'; import { round } from '$utils/format'; mapboxgl.accessToken = process.env.MAPBOX_TOKEN ?? ''; diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index 1cda917cd..7aabc4647 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect } from 'react'; import { useAtomValue } from 'jotai'; import styled, { css } from 'styled-components'; +import { MapRef } from 'react-map-gl'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { Button, createButtonStyles } from '@devseed-ui/button'; import bbox from '@turf/bbox'; @@ -8,32 +9,18 @@ import { CollecticonChartLine, CollecticonCircleInformation } from '@devseed-ui/collecticons'; -import useMaps from '$components/common/map/hooks/use-maps'; import { timelineDatasetsAtom } from '../../atoms/datasets'; import { selectedIntervalAtom } from '../../atoms/dates'; +import useMaps from '$components/common/map/hooks/use-maps'; import useAois from '$components/common/map/controls/hooks/use-aois'; -import { calcFeatCollArea, boundsFromFeature } from '$components/common/aoi/utils'; +import { calcFeatCollArea } from '$components/common/aoi/utils'; import { formatDateRange } from '$utils/date'; import { useAnalysisController } from '$components/exploration/hooks/use-analysis-data-request'; import useThemedControl from '$components/common/map/controls/hooks/use-themed-control'; +import { getZoomFromBbox } from '$components/common/map/utils'; import { AoIFeature } from '$components/common/map/types'; import { ShortcutCode } from '$styles/shortcut-code'; -export function getZoomFromBbox(bbox: [number, number, number, number]): number { - const latMax = Math.max(bbox[3], bbox[1]); - const lngMax = Math.max(bbox[2], bbox[0]); - const latMin = Math.min(bbox[3], bbox[1]); - const lngMin = Math.min(bbox[2], bbox[0]); - const maxDiff = Math.max(latMax - latMin, lngMax - lngMin); - if (maxDiff < 360 / Math.pow(2, 20)) { - return 21; -} else { - const zoomLevel = Math.floor(-1*( (Math.log(maxDiff)/Math.log(2)) - (Math.log(360)/Math.log(2)))); - if (zoomLevel < 1) return 1; - else return zoomLevel; - } -} - const AnalysisMessageWrapper = styled.div.attrs({ 'data-tour': 'analysis-message' @@ -97,7 +84,7 @@ const MessageControls = styled.div` gap: ${glsp(0.5)}; `; -export function AnalysisMessage({ maps }) { +export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { const { isObsolete, setObsolete, isAnalyzing } = useAnalysisController(); const datasets = useAtomValue(timelineDatasetsAtom); @@ -115,15 +102,27 @@ export function AnalysisMessage({ maps }) { setObsolete(); }, [setObsolete, features]); + const analysisCallback = useCallback(() => { + const bboxToFit = bbox({ + type: 'FeatureCollection', + features: selectedFeatures + }); + const zoom = bboxToFit? getZoomFromBbox(bboxToFit): 14; + mainMap?.flyTo({ + center:[ (bboxToFit[2] + bboxToFit[0])/2, (bboxToFit[3] + bboxToFit[1])/2], + zoom + }); + }, [selectedFeatures, mainMap]); + if (isAnalyzing) { return ( ); } else { @@ -131,17 +130,17 @@ export function AnalysisMessage({ maps }) { ); } } export function AnalysisMessageControl() { - const maps = useMaps() - useThemedControl(() => , { position: 'top-left' }); + const { main } = useMaps(); + useThemedControl(() => , { position: 'top-left' }); return null; } @@ -152,12 +151,13 @@ interface MessagesProps { selectedFeatures: AoIFeature[]; datasetIds: string[]; dateLabel: string | null; + analysisCallback: () => void; } function MessagesWhileAnalyzing( props: MessagesProps & { isObsolete: boolean } ) { - const { isObsolete, features, selectedFeatures, datasetIds, dateLabel, mapInstance } = + const { isObsolete, features, selectedFeatures, datasetIds, dateLabel, analysisCallback } = props; const area = calcFeatCollArea({ @@ -205,7 +205,7 @@ function MessagesWhileAnalyzing( - + @@ -251,7 +251,7 @@ function MessagesWhileAnalyzing( } function MessagesWhileNotAnalyzing(props: MessagesProps) { - const { features, selectedFeatures, datasetIds, dateLabel, mapInstance } = props; + const { features, selectedFeatures, datasetIds, dateLabel, analysisCallback } = props; if (selectedFeatures.length) { // Not analyzing, but there are selected features. @@ -280,7 +280,7 @@ function MessagesWhileNotAnalyzing(props: MessagesProps) { - + ); @@ -345,23 +345,14 @@ const Btn = styled(Button)` } `; -function ButtonObsolete(props: { datasetIds: string[] }) { - const { datasetIds, features, mapInstance } = props; +function ButtonObsolete(props: { datasetIds: string[], analysisCallback: () => void }) { + const { datasetIds, analysisCallback } = props; const { runAnalysis } = useAnalysisController(); const handleClick = useCallback(() => { runAnalysis(datasetIds); - const bboxToFit = bbox({ - type: 'FeatureCollection', - features - }) - - const zoom = bboxToFit? getZoomFromBbox(bboxToFit): 14; - mapInstance?.flyTo({ - center:[ (bboxToFit[2] + bboxToFit[0])/2, (bboxToFit[3] + bboxToFit[1])/2], - zoom - }); - },[datasetIds, mapInstance, features]) + analysisCallback(); + },[datasetIds, analysisCallback, runAnalysis]); return ( void }) { + const { datasetIds, analysisCallback } = props; const { runAnalysis } = useAnalysisController(); const handleClick = useCallback(() => { runAnalysis(datasetIds); - const bboxToFit = bbox({ - type: 'FeatureCollection', - features - }) - - const zoom = bboxToFit? getZoomFromBbox(bboxToFit): 14; - mapInstance?.flyTo({ - center:[ (bboxToFit[2] + bboxToFit[0])/2, (bboxToFit[3] + bboxToFit[1])/2], - zoom - }); - },[datasetIds, mapInstance, features]) + analysisCallback(); + },[datasetIds, runAnalysis, analysisCallback]); return ( Date: Sun, 31 Mar 2024 12:06:06 +0900 Subject: [PATCH 3/7] Add TOI zoom behavior for testing --- .../components/exploration/atoms/timeline.ts | 4 ++ .../map/analysis-message-control.tsx | 39 ++++++++++++++++--- .../exploration/components/map/index.tsx | 5 ++- .../components/timeline/timeline.tsx | 32 ++++++++++++--- app/scripts/components/exploration/index.tsx | 6 ++- 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/app/scripts/components/exploration/atoms/timeline.ts b/app/scripts/components/exploration/atoms/timeline.ts index 0e559d424..a4b563846 100644 --- a/app/scripts/components/exploration/atoms/timeline.ts +++ b/app/scripts/components/exploration/atoms/timeline.ts @@ -12,6 +12,10 @@ export const zoomTransformAtom = atom({ k: 1 }); +//. TOI test +export const zoomBehaviorAtom = atom(undefined); + + // Width of the whole timeline item. Set via a size observer and then used to // compute the different element sizes. export const timelineWidthAtom = atom(undefined); diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index 7aabc4647..602c346a6 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect } from 'react'; -import { useAtomValue } from 'jotai'; +import { useAtomValue, useAtom } from 'jotai'; import styled, { css } from 'styled-components'; import { MapRef } from 'react-map-gl'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; @@ -11,7 +11,15 @@ import { } from '@devseed-ui/collecticons'; import { timelineDatasetsAtom } from '../../atoms/datasets'; import { selectedIntervalAtom } from '../../atoms/dates'; +// import { applyTransform, isEqualTransform, rescaleX } from './timeline-utils'; +import { rescaleX } from '$components/exploration/components/timeline/timeline-utils'; import useMaps from '$components/common/map/hooks/use-maps'; +import { + timelineSizesAtom, + timelineWidthAtom, + zoomTransformAtom +} from '$components/exploration/atoms/timeline'; +import { useScales } from '../../hooks/scales-hooks'; import useAois from '$components/common/map/controls/hooks/use-aois'; import { calcFeatCollArea } from '$components/common/aoi/utils'; @@ -21,6 +29,7 @@ import useThemedControl from '$components/common/map/controls/hooks/use-themed-c import { getZoomFromBbox } from '$components/common/map/utils'; import { AoIFeature } from '$components/common/map/types'; import { ShortcutCode } from '$styles/shortcut-code'; +import { RIGHT_AXIS_SPACE } from '$components/exploration/constants' const AnalysisMessageWrapper = styled.div.attrs({ 'data-tour': 'analysis-message' @@ -84,14 +93,19 @@ const MessageControls = styled.div` gap: ${glsp(0.5)}; `; -export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { +export function AnalysisMessage({ mainMap, zoomTOI }: { mainMap: MapRef | undefined }) { const { isObsolete, setObsolete, isAnalyzing } = useAnalysisController(); const datasets = useAtomValue(timelineDatasetsAtom); const datasetIds = datasets.map((d) => d.data.id); + const timelineWidth = useAtomValue(timelineWidthAtom); + const { main, scaled} = useScales(); + const [zoomTransform, setZoomTransform] = useAtom(zoomTransformAtom); + const { features } = useAois(); const selectedInterval = useAtomValue(selectedIntervalAtom); + const dateLabel = selectedInterval && formatDateRange(selectedInterval.start, selectedInterval.end); @@ -102,7 +116,10 @@ export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { setObsolete(); }, [setObsolete, features]); + + const analysisCallback = useCallback(() => { + // Fit AOI const bboxToFit = bbox({ type: 'FeatureCollection', features: selectedFeatures @@ -112,7 +129,18 @@ export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { center:[ (bboxToFit[2] + bboxToFit[0])/2, (bboxToFit[3] + bboxToFit[1])/2], zoom }); - }, [selectedFeatures, mainMap]); + + // Fit TOI + if (!main || !timelineWidth || !zoomTOI ) return + const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE) * 0.6 + const startPoint = (timelineWidth - RIGHT_AXIS_SPACE) * 0.1 + const new_k = widthToFit/(main(selectedInterval.end) - main(selectedInterval.start)); + const new_x = startPoint - new_k * main(selectedInterval.start); + + + zoomTOI(new_x, new_k); + + }, [selectedFeatures, mainMap, main, timelineWidth, zoomTOI, selectedInterval]); if (isAnalyzing) { return ( @@ -138,9 +166,10 @@ export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { } } -export function AnalysisMessageControl() { +export function AnalysisMessageControl(props) { const { main } = useMaps(); - useThemedControl(() => , { position: 'top-left' }); + const { zoomTOI } = props; + useThemedControl(() => , { position: 'top-left' }); return null; } diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 35de27630..6712db212 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -27,7 +27,8 @@ import DrawControl from '$components/common/map/controls/aoi'; import CustomAoIControl from '$components/common/map/controls/aoi/custom-aoi-control'; import { usePreviousValue } from '$utils/use-effect-previous'; -export function ExplorationMap() { +export function ExplorationMap(props) { + const { zoomTOI } = props; const [projection, setProjection] = useState(projectionDefault); const { @@ -144,7 +145,7 @@ export function ExplorationMap() { comparing && 'Analysis is not possible when comparing dates' } /> - + { }; export default function Timeline(props: TimelineProps) { - const { onDatasetAddClick } = props; + const { onDatasetAddClick, setZoomTOIFunc } = props; // Refs for non react based interactions. // The interaction rect is used to capture the different d3 events for the @@ -200,7 +201,7 @@ export default function Timeline(props: TimelineProps) { const [selectedInterval, setSelectedInterval] = useAtom(selectedIntervalAtom); const { setObsolete, runAnalysis, isAnalyzing } = useAnalysisController(); - + const [zoomTransform, setZoomTransform] = useAtom(zoomTransformAtom); const { features } = useAois(); useEffect(() => { @@ -216,13 +217,15 @@ export default function Timeline(props: TimelineProps) { [width] ); - const [zoomTransform, setZoomTransform] = useAtom(zoomTransformAtom); - // Calculate min and max scale factors, such has each day has a minimum of 2px // and a maximum of 100px. const { k0, k1 } = useScaleFactors(); const { scaled: xScaled, main: xMain } = useScales(); + // const zoomBehavior = useZoomBehavior({k0, k1, translateExtent, datasetsContainerRef}) + // console.log(zoomBehavior) + // console.log(typeof zoomBehavior) + // setZoomBehaviorAtom(zoomBehavior(datasetsContainerRef)); // Create the zoom behavior needed for the timeline interactions. const zoomBehavior = useMemo(() => { return zoom() @@ -371,7 +374,24 @@ export default function Timeline(props: TimelineProps) { applyTransform(zoomBehavior, select(interactionRef.current), 0, 0, k0); }, [prevSuccessDatasetsCount, successDatasetsCount, k0, zoomBehavior]); - const onControlsZoom = useCallback( + const onTOIZoom = useCallback(()=> (newX, newK) => { + if (!newX || ! newK) return; + applyTransform( + zoomBehavior, + select(interactionRef.current), + newX, + 0, + newK + ); + },[interactionRef.current, zoomBehavior]) + +// Pass onTOIZoom up to the parent component when it's defined or changed + useEffect(() => { + if (!onTOIZoom) return + setZoomTOIFunc(onTOIZoom); + }, [onTOIZoom, setZoomTOIFunc]); + + const onControlsZoom = useCallback( (zoomV) => { if (!interactionRef.current || !xMain || !xScaled || !selectedDay) return; diff --git a/app/scripts/components/exploration/index.tsx b/app/scripts/components/exploration/index.tsx index 4b8a6bb8a..9b80954b7 100644 --- a/app/scripts/components/exploration/index.tsx +++ b/app/scripts/components/exploration/index.tsx @@ -79,6 +79,8 @@ function Exploration() { const setUrl = useSetAtom(urlAtom); const { reset: resetAnalysisController } = useAnalysisController(); + + const [zoomTOIFunc, setZoomTOIFunc] = useState(() => {}); // Reset atoms when leaving the page. useEffect(() => { return () => { @@ -105,11 +107,11 @@ function Exploration() { - + - + From 0219b90e35bbe947e25cd01cf57962dc29f93988 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Sun, 31 Mar 2024 13:20:06 +0900 Subject: [PATCH 4/7] Tweak values --- .../exploration/components/map/analysis-message-control.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index 602c346a6..77e533a51 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -132,8 +132,8 @@ export function AnalysisMessage({ mainMap, zoomTOI }: { mainMap: MapRef | undefi // Fit TOI if (!main || !timelineWidth || !zoomTOI ) return - const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE) * 0.6 - const startPoint = (timelineWidth - RIGHT_AXIS_SPACE) * 0.1 + const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE) * 0.8 + const startPoint = (timelineWidth - RIGHT_AXIS_SPACE) * 0.05 const new_k = widthToFit/(main(selectedInterval.end) - main(selectedInterval.start)); const new_x = startPoint - new_k * main(selectedInterval.start); From 7a05f659b16a56dafd68d27ccf5bcbb54e75993e Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 2 Apr 2024 14:30:56 +0900 Subject: [PATCH 5/7] Move TOI function to atom --- .../components/exploration/atoms/timeline.ts | 5 +-- .../map/analysis-message-control.tsx | 36 +++++++++---------- .../exploration/components/map/index.tsx | 5 ++- .../components/timeline/timeline.tsx | 27 ++++++-------- .../exploration/hooks/use-toi-zoom.ts | 33 +++++++++++++++++ app/scripts/components/exploration/index.tsx | 6 ++-- 6 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 app/scripts/components/exploration/hooks/use-toi-zoom.ts diff --git a/app/scripts/components/exploration/atoms/timeline.ts b/app/scripts/components/exploration/atoms/timeline.ts index a4b563846..97237f34c 100644 --- a/app/scripts/components/exploration/atoms/timeline.ts +++ b/app/scripts/components/exploration/atoms/timeline.ts @@ -12,9 +12,10 @@ export const zoomTransformAtom = atom({ k: 1 }); -//. TOI test -export const zoomBehaviorAtom = atom(undefined); +// Atom to zoom TOI when analysis is run +type ZoomTOIFunction = (newX: number, newK: number) => void; +export const onTOIZoomAtom = atom(null); // Width of the whole timeline item. Set via a size observer and then used to // compute the different element sizes. diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index 77e533a51..a36aa0372 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect } from 'react'; -import { useAtomValue, useAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; import styled, { css } from 'styled-components'; import { MapRef } from 'react-map-gl'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; @@ -11,15 +11,13 @@ import { } from '@devseed-ui/collecticons'; import { timelineDatasetsAtom } from '../../atoms/datasets'; import { selectedIntervalAtom } from '../../atoms/dates'; -// import { applyTransform, isEqualTransform, rescaleX } from './timeline-utils'; -import { rescaleX } from '$components/exploration/components/timeline/timeline-utils'; + +import { useScales } from '../../hooks/scales-hooks'; import useMaps from '$components/common/map/hooks/use-maps'; +import { useOnTOIZoom } from '$components/exploration/hooks/use-toi-zoom'; import { - timelineSizesAtom, - timelineWidthAtom, - zoomTransformAtom + timelineWidthAtom } from '$components/exploration/atoms/timeline'; -import { useScales } from '../../hooks/scales-hooks'; import useAois from '$components/common/map/controls/hooks/use-aois'; import { calcFeatCollArea } from '$components/common/aoi/utils'; @@ -29,7 +27,7 @@ import useThemedControl from '$components/common/map/controls/hooks/use-themed-c import { getZoomFromBbox } from '$components/common/map/utils'; import { AoIFeature } from '$components/common/map/types'; import { ShortcutCode } from '$styles/shortcut-code'; -import { RIGHT_AXIS_SPACE } from '$components/exploration/constants' +import { RIGHT_AXIS_SPACE } from '$components/exploration/constants'; const AnalysisMessageWrapper = styled.div.attrs({ 'data-tour': 'analysis-message' @@ -93,15 +91,15 @@ const MessageControls = styled.div` gap: ${glsp(0.5)}; `; -export function AnalysisMessage({ mainMap, zoomTOI }: { mainMap: MapRef | undefined }) { +export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { const { isObsolete, setObsolete, isAnalyzing } = useAnalysisController(); const datasets = useAtomValue(timelineDatasetsAtom); const datasetIds = datasets.map((d) => d.data.id); const timelineWidth = useAtomValue(timelineWidthAtom); - const { main, scaled} = useScales(); - const [zoomTransform, setZoomTransform] = useAtom(zoomTransformAtom); + const { main } = useScales(); + const { onTOIZoom } = useOnTOIZoom(); const { features } = useAois(); const selectedInterval = useAtomValue(selectedIntervalAtom); @@ -131,16 +129,15 @@ export function AnalysisMessage({ mainMap, zoomTOI }: { mainMap: MapRef | undefi }); // Fit TOI - if (!main || !timelineWidth || !zoomTOI ) return - const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE) * 0.8 - const startPoint = (timelineWidth - RIGHT_AXIS_SPACE) * 0.05 + if (!main || !timelineWidth || !selectedInterval?.start ) return; + const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE) * 0.8; + const startPoint = (timelineWidth - RIGHT_AXIS_SPACE) * 0.05; const new_k = widthToFit/(main(selectedInterval.end) - main(selectedInterval.start)); const new_x = startPoint - new_k * main(selectedInterval.start); - - zoomTOI(new_x, new_k); + onTOIZoom(new_x, new_k); - }, [selectedFeatures, mainMap, main, timelineWidth, zoomTOI, selectedInterval]); + }, [selectedFeatures, mainMap, main, timelineWidth, onTOIZoom, selectedInterval]); if (isAnalyzing) { return ( @@ -166,10 +163,9 @@ export function AnalysisMessage({ mainMap, zoomTOI }: { mainMap: MapRef | undefi } } -export function AnalysisMessageControl(props) { +export function AnalysisMessageControl() { const { main } = useMaps(); - const { zoomTOI } = props; - useThemedControl(() => , { position: 'top-left' }); + useThemedControl(() => , { position: 'top-left' }); return null; } diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 6712db212..35de27630 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -27,8 +27,7 @@ import DrawControl from '$components/common/map/controls/aoi'; import CustomAoIControl from '$components/common/map/controls/aoi/custom-aoi-control'; import { usePreviousValue } from '$utils/use-effect-previous'; -export function ExplorationMap(props) { - const { zoomTOI } = props; +export function ExplorationMap() { const [projection, setProjection] = useState(projectionDefault); const { @@ -145,7 +144,7 @@ export function ExplorationMap(props) { comparing && 'Analysis is not possible when comparing dates' } /> - + { }; export default function Timeline(props: TimelineProps) { - const { onDatasetAddClick, setZoomTOIFunc } = props; + const { onDatasetAddClick } = props; // Refs for non react based interactions. // The interaction rect is used to capture the different d3 events for the @@ -374,22 +374,15 @@ export default function Timeline(props: TimelineProps) { applyTransform(zoomBehavior, select(interactionRef.current), 0, 0, k0); }, [prevSuccessDatasetsCount, successDatasetsCount, k0, zoomBehavior]); - const onTOIZoom = useCallback(()=> (newX, newK) => { - if (!newX || ! newK) return; - applyTransform( - zoomBehavior, - select(interactionRef.current), - newX, - 0, - newK - ); - },[interactionRef.current, zoomBehavior]) + const { initializeTOIZoom } = useOnTOIZoom(); -// Pass onTOIZoom up to the parent component when it's defined or changed useEffect(() => { - if (!onTOIZoom) return - setZoomTOIFunc(onTOIZoom); - }, [onTOIZoom, setZoomTOIFunc]); + // Set TOIZoom functionality in atom so it can be used in analysis component + // Ensure zoomBehavior and interactionRef are defined before initializing + if (zoomBehavior && interactionRef.current) { + initializeTOIZoom(zoomBehavior, interactionRef); + } + }, [initializeTOIZoom, zoomBehavior, interactionRef]); const onControlsZoom = useCallback( (zoomV) => { diff --git a/app/scripts/components/exploration/hooks/use-toi-zoom.ts b/app/scripts/components/exploration/hooks/use-toi-zoom.ts new file mode 100644 index 000000000..2181f385b --- /dev/null +++ b/app/scripts/components/exploration/hooks/use-toi-zoom.ts @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; +import { useAtom } from 'jotai'; +import { select, ZoomBehavior } from 'd3'; +import { onTOIZoomAtom } from '../atoms/timeline'; +import { applyTransform } from '$components/exploration/components/timeline/timeline-utils'; + +export function useOnTOIZoom() { + const [onTOIZoom, setOnTOIZoom] = useAtom(onTOIZoomAtom); + + const initialize = useCallback((zoomBehavior: ZoomBehavior, interactionRef: React.RefObject) => { + setOnTOIZoom(() => (newX: number, newK: number) => { + if (!newX || !newK) return; + const { current: interactionElement } = interactionRef; + if (!interactionElement) return; + + applyTransform( + zoomBehavior, + select(interactionElement), + newX, + 0, + newK + ); + }); + }, [setOnTOIZoom]); + + const safeOnTOIZoom = (newX: number, newK: number) => { + if (onTOIZoom) { + onTOIZoom(newX, newK); + } + }; + + return { initializeTOIZoom: initialize, onTOIZoom: safeOnTOIZoom }; +} diff --git a/app/scripts/components/exploration/index.tsx b/app/scripts/components/exploration/index.tsx index 9b80954b7..4b8a6bb8a 100644 --- a/app/scripts/components/exploration/index.tsx +++ b/app/scripts/components/exploration/index.tsx @@ -79,8 +79,6 @@ function Exploration() { const setUrl = useSetAtom(urlAtom); const { reset: resetAnalysisController } = useAnalysisController(); - - const [zoomTOIFunc, setZoomTOIFunc] = useState(() => {}); // Reset atoms when leaving the page. useEffect(() => { return () => { @@ -107,11 +105,11 @@ function Exploration() { - + - + From 206bcfba148754f087c6d41aed540f747c7eaded Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 2 Apr 2024 18:13:03 +0900 Subject: [PATCH 6/7] Adjust number for toi fit --- .../components/map/analysis-message-control.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index a36aa0372..5933d15fe 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -27,7 +27,7 @@ import useThemedControl from '$components/common/map/controls/hooks/use-themed-c import { getZoomFromBbox } from '$components/common/map/utils'; import { AoIFeature } from '$components/common/map/types'; import { ShortcutCode } from '$styles/shortcut-code'; -import { RIGHT_AXIS_SPACE } from '$components/exploration/constants'; +import { RIGHT_AXIS_SPACE, HEADER_COLUMN_WIDTH } from '$components/exploration/constants'; const AnalysisMessageWrapper = styled.div.attrs({ 'data-tour': 'analysis-message' @@ -130,8 +130,9 @@ export function AnalysisMessage({ mainMap }: { mainMap: MapRef | undefined }) { // Fit TOI if (!main || !timelineWidth || !selectedInterval?.start ) return; - const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE) * 0.8; - const startPoint = (timelineWidth - RIGHT_AXIS_SPACE) * 0.05; + + const widthToFit = (timelineWidth - RIGHT_AXIS_SPACE - HEADER_COLUMN_WIDTH) * 0.9; + const startPoint = 0; const new_k = widthToFit/(main(selectedInterval.end) - main(selectedInterval.start)); const new_x = startPoint - new_k * main(selectedInterval.start); From 6f0eda216dad573741f05120a29d9ba5cc6c9141 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 2 Apr 2024 22:41:52 +0900 Subject: [PATCH 7/7] Remove unnecessary comments --- .../components/exploration/components/timeline/timeline.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/scripts/components/exploration/components/timeline/timeline.tsx b/app/scripts/components/exploration/components/timeline/timeline.tsx index 3da60d519..d96050364 100644 --- a/app/scripts/components/exploration/components/timeline/timeline.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline.tsx @@ -222,10 +222,6 @@ export default function Timeline(props: TimelineProps) { const { k0, k1 } = useScaleFactors(); const { scaled: xScaled, main: xMain } = useScales(); - // const zoomBehavior = useZoomBehavior({k0, k1, translateExtent, datasetsContainerRef}) - // console.log(zoomBehavior) - // console.log(typeof zoomBehavior) - // setZoomBehaviorAtom(zoomBehavior(datasetsContainerRef)); // Create the zoom behavior needed for the timeline interactions. const zoomBehavior = useMemo(() => { return zoom()