Skip to content

Commit

Permalink
feat: fetches and caches esri wayback data for project time travel co…
Browse files Browse the repository at this point in the history
…nfig
  • Loading branch information
mohitb35 committed Jan 21, 2025
1 parent 49d4d98 commit 553d5fb
Show file tree
Hide file tree
Showing 6 changed files with 1,719 additions and 1,874 deletions.
3,405 changes: 1,567 additions & 1,838 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@types/react-gtm-module": "^2.0.1",
"@types/react-lazyload": "^2.6.0",
"@types/supercluster": "^7.1.0",
"@vannizhang/wayback-core": "^1.0.8",
"@vercel/kv": "^1.0.1",
"apexcharts": "^3.54.0",
"connection-string": "^4.3.6",
Expand Down
33 changes: 27 additions & 6 deletions src/features/projectsV2/ProjectDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { getPlantData } from '../../../utils/projectV2';
import ProjectDetailsMeta from '../../../utils/getMetaTags/ProjectDetailsMeta';
import OtherInterventionInfo from './components/OtherInterventionInfo';
import { isNonPlantationType } from '../../../utils/constants/intervention';
import { getProjectTimeTravelConfig } from '../../../utils/mapsV2/timeTravel';
import { useProjectsMap } from '../ProjectsMapContext';

const ProjectDetails = ({
currencyCode,
Expand All @@ -47,6 +49,7 @@ const ProjectDetails = ({
setSelectedSamplePlantLocation,
setPreventShallowPush,
} = useProjects();
const { setTimeTravelConfig } = useProjectsMap();
const { setErrors, redirect } = useContext(ErrorHandlingContext);
const { tenantConfig } = useTenant();
const locale = useLocale();
Expand All @@ -71,9 +74,17 @@ const ProjectDetails = ({
locale: locale,
},
});

const { purpose } = fetchedProject;
if (purpose === 'conservation' || purpose === 'trees') {
setSingleProject(fetchedProject);
const timeTravelConfig = await getProjectTimeTravelConfig(
fetchedProject.id,
fetchedProject.geoLocation
);
if (Object.keys(timeTravelConfig).length > 0) {
setTimeTravelConfig(timeTravelConfig);
}
} else {
throw new ClientError(404, {
error_type: 'project_not_available',
Expand Down Expand Up @@ -184,8 +195,8 @@ const ProjectDetails = ({
hoveredPlantLocation?.type === 'multi-tree-registration'
? hoveredPlantLocation
: selectedPlantLocation?.type === 'multi-tree-registration'
? selectedPlantLocation
: undefined
? selectedPlantLocation
: undefined
}
setSelectedSamplePlantLocation={setSelectedSamplePlantLocation}
isMobile={isMobile}
Expand All @@ -194,10 +205,20 @@ const ProjectDetails = ({

{shouldShowOtherIntervention ? (
<OtherInterventionInfo
selectedPlantLocation={selectedPlantLocation && selectedPlantLocation?.type !== 'single-tree-registration' &&
selectedPlantLocation?.type !== 'multi-tree-registration' ? selectedPlantLocation : null}
hoveredPlantLocation={hoveredPlantLocation && hoveredPlantLocation?.type !== 'single-tree-registration' &&
hoveredPlantLocation?.type !== 'multi-tree-registration' ? hoveredPlantLocation : null}
selectedPlantLocation={
selectedPlantLocation &&
selectedPlantLocation?.type !== 'single-tree-registration' &&
selectedPlantLocation?.type !== 'multi-tree-registration'
? selectedPlantLocation
: null
}
hoveredPlantLocation={
hoveredPlantLocation &&
hoveredPlantLocation?.type !== 'single-tree-registration' &&
hoveredPlantLocation?.type !== 'multi-tree-registration'
? hoveredPlantLocation
: null
}
setSelectedSamplePlantLocation={setSelectedSamplePlantLocation}
isMobile={isMobile}
/>
Expand Down
61 changes: 31 additions & 30 deletions src/features/projectsV2/ProjectsMap/TimeTravel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import type {
SourceName,
TimeTravelConfig,
ProjectTimeTravelConfig,
} from '../../../../utils/mapsV2/timeTravel';
import type { FeatureCollection, MultiPolygon, Polygon } from 'geojson';
import type { ProjectSite } from '@planet-sdk/common/build/types/project';
Expand All @@ -18,7 +18,6 @@ import MaplibreCompare from '@maplibre/maplibre-gl-compare';
import '@maplibre/maplibre-gl-compare/dist/maplibre-gl-compare.css';
import { Map } from 'maplibre-gl';
import { useProjectsMap } from '../../ProjectsMapContext';
import { getTimeTravelConfig } from '../../../../utils/mapsV2/timeTravel';
import { ErrorHandlingContext } from '../../../common/Layout/ErrorHandlingContext';
import TimeTravelDropdown from '../../TimeTravelDropdown';
import styles from './TimeTravel.module.scss';
Expand Down Expand Up @@ -51,7 +50,8 @@ export default function TimeTravel({
sitesGeoJson,
isVisible,
}: Props): ReactElement {
const { viewState: mainMapViewState } = useProjectsMap();
const { viewState: mainMapViewState, timeTravelConfig } = useProjectsMap();

const { setErrors } = useContext(ErrorHandlingContext);

const comparisonContainer = useRef<HTMLDivElement>(null);
Expand All @@ -67,23 +67,23 @@ export default function TimeTravel({
const [beforeDropdownOpen, setBeforeDropdownOpen] = useState(false);
const [afterDropdownOpen, setAfterDropdownOpen] = useState(false);

const timeTravelData = useMemo(() => getTimeTravelConfig(), []);

const availableYears = useMemo(
() => timeTravelData[DEFAULT_SOURCE]?.map((item) => item.year) || [],
[timeTravelData]
);
const availableYears = useMemo(() => {
if (timeTravelConfig === null) return [];
return timeTravelConfig[DEFAULT_SOURCE]?.map((item) => item.year) || [];
}, [timeTravelConfig]);

const availableSources = useMemo(
() => Object.keys(timeTravelData) as Array<keyof TimeTravelConfig>,
[timeTravelData]
);
const availableSources = useMemo(() => {
if (timeTravelConfig === null) return [];
return Object.keys(timeTravelConfig) as Array<
keyof ProjectTimeTravelConfig
>;
}, [timeTravelConfig]);

const [selectedSourceBefore, setSelectedSourceBefore] = useState<
keyof TimeTravelConfig
keyof ProjectTimeTravelConfig
>(availableSources[0] || DEFAULT_SOURCE);
const [selectedSourceAfter, setSelectedSourceAfter] = useState<
keyof TimeTravelConfig
keyof ProjectTimeTravelConfig
>(availableSources[0] || DEFAULT_SOURCE);
const [selectedYearBefore, setSelectedYearBefore] = useState(
availableYears[0] || DEFAULT_BEFORE_YEAR
Expand All @@ -104,11 +104,11 @@ export default function TimeTravel({
throw new Error('Invalid or missing GeoJSON data');
}

if (!timeTravelData || Object.keys(timeTravelData).length === 0) {
if (!timeTravelConfig || Object.keys(timeTravelConfig).length === 0) {
throw new Error('Time travel configuration not available');
}

const beforeYearExists = timeTravelData[selectedSourceBefore]?.some(
const beforeYearExists = timeTravelConfig[selectedSourceBefore]?.some(
(item) => item.year === selectedYearBefore
);
if (!beforeYearExists) {
Expand All @@ -117,7 +117,7 @@ export default function TimeTravel({
);
}

const afterYearExists = timeTravelData[selectedSourceAfter]?.some(
const afterYearExists = timeTravelConfig[selectedSourceAfter]?.some(
(item) => item.year === selectedYearAfter
);
if (!afterYearExists) {
Expand All @@ -126,7 +126,7 @@ export default function TimeTravel({
);
}
}, [
timeTravelData,
timeTravelConfig,
selectedSourceBefore,
selectedSourceAfter,
selectedYearBefore,
Expand All @@ -136,7 +136,7 @@ export default function TimeTravel({

// Initialize the side by side comparison map
useEffect(() => {
if (typeof window === 'undefined') return;
if (typeof window === 'undefined' || !timeTravelConfig) return;

try {
validateData();
Expand Down Expand Up @@ -252,15 +252,16 @@ export default function TimeTravel({
}, [beforeLoaded, afterLoaded]);

const loadLayers = useCallback(() => {
if (!beforeMapRef.current || !afterMapRef.current) return;
if (!timeTravelConfig || !beforeMapRef.current || !afterMapRef.current)
return;

try {
validateData();

// Handle before map layers
if (timeTravelData.esri) {
if (timeTravelConfig.esri) {

Check notice on line 262 in src/features/projectsV2/ProjectsMap/TimeTravel/index.tsx

View check run for this annotation

codefactor.io / CodeFactor

src/features/projectsV2/ProjectsMap/TimeTravel/index.tsx#L254-L262

Complex Method
// First remove all existing layers from before map
timeTravelData.esri.forEach((year) => {
timeTravelConfig.esri.forEach((year) => {
const layerId = `before-imagery-esri-${year.year}-layer`;
const polygonLayerId = `project-polygon-layer-esri-${year.year}`;

Expand All @@ -274,7 +275,7 @@ export default function TimeTravel({

// Add new layers if source is esri
if (selectedSourceBefore === 'esri') {
const beforeYear = timeTravelData.esri.find(
const beforeYear = timeTravelConfig.esri.find(
(year) => year.year === selectedYearBefore
);

Expand All @@ -292,7 +293,7 @@ export default function TimeTravel({
if (!beforeMapRef.current.getSource(sourceId)) {
beforeMapRef.current.addSource(sourceId, {
type: 'raster',
tiles: [beforeYear.raster],
tiles: [beforeYear.rasterUrl],
tileSize: 256,
attribution: 'layer attribution',
});
Expand Down Expand Up @@ -329,9 +330,9 @@ export default function TimeTravel({
}

// Handle after map layers (similar logic)
if (timeTravelData.esri) {
if (timeTravelConfig.esri) {
// First remove all existing layers from after map
timeTravelData.esri.forEach((year) => {
timeTravelConfig.esri.forEach((year) => {
const layerId = `after-imagery-esri-${year.year}-layer`;
const polygonLayerId = `project-polygon-layer-esri-${year.year}`;

Expand All @@ -345,7 +346,7 @@ export default function TimeTravel({

// Add new layers if source is esri
if (selectedSourceAfter === 'esri') {
const afterYear = timeTravelData.esri.find(
const afterYear = timeTravelConfig.esri.find(
(year) => year.year === selectedYearAfter
);

Expand All @@ -363,7 +364,7 @@ export default function TimeTravel({
if (!afterMapRef.current.getSource(sourceId)) {
afterMapRef.current.addSource(sourceId, {
type: 'raster',
tiles: [afterYear.raster],
tiles: [afterYear.rasterUrl],
tileSize: 256,
attribution: 'layer attribution',
});
Expand Down Expand Up @@ -407,7 +408,7 @@ export default function TimeTravel({
);
}
}, [
timeTravelData,
timeTravelConfig,
selectedSourceBefore,
selectedSourceAfter,
selectedYearBefore,
Expand Down
7 changes: 7 additions & 0 deletions src/features/projectsV2/ProjectsMapContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { FC } from 'react';
import type { ViewState } from 'react-map-gl-v7';
import type { MapStyle } from 'react-map-gl-v7/maplibre';
import type { SetState } from '../common/types/common';
import type { ProjectTimeTravelConfig } from '../../utils/mapsV2/timeTravel';

import { useContext, useMemo, createContext, useState, useEffect } from 'react';
import getMapStyle from '../../utils/maps/getMapStyle';
Expand Down Expand Up @@ -63,6 +64,8 @@ interface ProjectsMapState {
* Updates the state of a single map-related option.
*/
updateMapOption: (option: keyof MapOptions, value: boolean) => void;
timeTravelConfig: ProjectTimeTravelConfig | null;
setTimeTravelConfig: SetState<ProjectTimeTravelConfig | null>;
}

const ProjectsMapContext = createContext<ProjectsMapState | null>(null);
Expand All @@ -73,6 +76,8 @@ export const ProjectsMapProvider: FC = ({ children }) => {
const [mapOptions, setMapOptions] = useState<MapOptions>({
showProjects: true,
});
const [timeTravelConfig, setTimeTravelConfig] =
useState<ProjectTimeTravelConfig | null>(null);

const handleViewStateChange = (newViewState: Partial<ExtendedViewState>) => {
setViewState((prev) => ({
Expand Down Expand Up @@ -109,6 +114,8 @@ export const ProjectsMapProvider: FC = ({ children }) => {
setIsSatelliteView,
mapOptions,
updateMapOption,
timeTravelConfig,
setTimeTravelConfig,
}),
[mapState, viewState, mapOptions, isSatelliteView]
);
Expand Down
86 changes: 86 additions & 0 deletions src/utils/mapsV2/timeTravel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { WaybackItem } from '@vannizhang/wayback-core';
import type { Point } from 'geojson';

import timeTravelConfig from '../../../public/data/maps/time-travel.json';
import { getWaybackItemsWithLocalChanges } from '@vannizhang/wayback-core';
import { cacheKeyPrefix } from '../constants/cacheKeyPrefix';
import { getCachedData } from '../../server/utils/cache';

const SOURCE_NAMES = ['esri'] as const;

Expand Down Expand Up @@ -34,3 +40,83 @@ export const getTimeTravelConfig = (): TimeTravelConfig => {

return result;
};

/**
* Converts WMTS URL format ({level}/{row}/{col}) to standard tile format (z/y/x)
*/
const convertToZYXFormat = (url: string): string => {
return url
.replace('{level}', '{z}')
.replace('{row}', '{y}')
.replace('{col}', '{x}');
};

interface SingleYearTimeTravelData {
year: string;
rasterUrl: string;
}

const getLatestByYear = (items: WaybackItem[]): SingleYearTimeTravelData[] => {
const intermediate = items.reduce<
Record<string, { rasterUrl: string; timestamp: number }>
>((acc, item) => {
const year = new Date(item.releaseDatetime).getFullYear().toString();
const existing = acc[year];

if (!existing || item.releaseDatetime > existing.timestamp) {
acc[year] = {
rasterUrl: convertToZYXFormat(item.itemURL),
timestamp: item.releaseDatetime,
};
}

return acc;
}, {});

// Transform to array format
return Object.entries(intermediate).map(([year, item]) => ({
year,
rasterUrl: item.rasterUrl,
}));
};

export type ProjectTimeTravelConfig = {
[key in SourceName]?: SingleYearTimeTravelData[];
};

export const getProjectTimeTravelConfig = async (
projectId: string,
projectPointGeometry: Point
): Promise<ProjectTimeTravelConfig> => {
const CACHE_KEY = `${cacheKeyPrefix}_time-travel_${projectId}`;
// TODO - change temp cache time
const CACHE_TIME_IN_SECONDS = 60 * 5; /* 60 * 60 * 24 */

async function fetchTimeTravelData(): Promise<ProjectTimeTravelConfig> {
const esriWaybackItems = await getWaybackItemsWithLocalChanges(
{
longitude: projectPointGeometry.coordinates[0],
latitude: projectPointGeometry.coordinates[1],
},
13 //TODO - confirm zoom level and update
);

if (esriWaybackItems.length === 0) {
return {};
} else {
return { esri: getLatestByYear(esriWaybackItems) };
}
}

try {
return await getCachedData(
CACHE_KEY,
fetchTimeTravelData,
CACHE_TIME_IN_SECONDS
);
} catch (err) {
console.error('Error fetching time travel data:', err);
// Return empty config on error to gracefully degrade
return {};
}
};

0 comments on commit 553d5fb

Please sign in to comment.