Skip to content

Commit

Permalink
Merge branch 'main' into 188059331-get-data-button-in-sim
Browse files Browse the repository at this point in the history
  • Loading branch information
bacalj committed Aug 19, 2024
2 parents 6efc8c8 + 997f273 commit 8cec602
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 122 deletions.
154 changes: 106 additions & 48 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState, useRef, useCallback } from "react";
import { clsx } from "clsx";
import { ILocation } from "../types";
import { kInitialDimensions, kVersion, kPluginName, kDefaultOnAttributes, kSimulationTabDimensions, kDataContextName } from "../constants";
import { initializePlugin, codapInterface, addDataContextChangeListener, ClientNotification } from "@concord-consortium/codap-plugin-api";
import { ICurrentDayLocation, ILocation } from "../types";
import { debounce } from "../grasp-seasons/utils/utils";
import { kInitialDimensions, kVersion, kPluginName, kDefaultOnAttributes, kSimulationTabDimensions, kDataContextName, kChildCollectionName } from "../constants";
import { initializePlugin, codapInterface, selectSelf, addDataContextChangeListener, ClientNotification, getCaseByID } from "@concord-consortium/codap-plugin-api";
import { useCodapData } from "../hooks/useCodapData";
import { LocationTab } from "./location-tab";
import { SimulationTab } from "./simulation-tab";
import { Header } from "./header";

import "../assets/scss/App.scss";

const updateRowSelectionInCodap = (latitude: string, longitude: string, day: number) => {
// TODO: Issue CODAP API request to highlight appropriate case in the case table, using combination of
// Math.floor(day), latitude, and longitude.
}
const debouncedUpdateRowSelectionInCodap = debounce((
latitude: string,
longitude: string,
day: number
) => {
codapInterface.sendRequest({
action: "get",
resource: `dataContext[${kDataContextName}].collection[${kChildCollectionName}].caseSearch[calcId==${latitude},${longitude},${Math.floor(day)}]`
}).then((result: any) => {
if (result.success && result.values.length > 0) {
const caseID = result.values[0].id;
return codapInterface.sendRequest({
action: "create",
resource: `dataContext[${kDataContextName}].selectionList`,
values: [caseID]
});
} else {
return null;
}
}).then((selectionResult: any) => {
if (!selectionResult.success) {
console.warn("Selection result was not successful", selectionResult);
}
}).catch((error: any) => {
console.error("Error in selection process:", error);
});
}, 250);

export const App: React.FC = () => {
const [activeTab, setActiveTab] = useState<"location" | "simulation">("location");
Expand All @@ -25,33 +49,37 @@ export const App: React.FC = () => {
const [selectedAttrs, setSelectedAttributes] = useState<string[]>(kDefaultOnAttributes);
const [dataContext, setDataContext] = useState<any>(null);

const currentDayLocationRef = useRef<ICurrentDayLocation>({
_latitude: "",
_longitude: "",
_dayOfYear: 171
});

const { getUniqueLocationsInCodapData } = useCodapData();
const getUniqueLocationsRef = useRef(getUniqueLocationsInCodapData);

const handleDayUpdateInTheSimTab = (day: number) => {
console.log("The day of the year has been updated in the simulation tab to: ", day); // TODO: remove it later
// We might to debounce this call, as if the animation is on, or user is dragging the slider, there will be
// lot of events and API calls to CODAP.
updateRowSelectionInCodap(latitude, longitude, Math.floor(day));
// Note that we do not need to update dayOfYear state variable. It's useful only for the opposite direction
// of the sync process, when user select a row in CODAP and we want to update the day in the simulation tab.
currentDayLocationRef.current._dayOfYear = day;
debouncedUpdateRowSelectionInCodap(
currentDayLocationRef.current._latitude,
currentDayLocationRef.current._longitude,
day
);
};

const handleCaseSelectionInCodap = (_latitude: string, _longitude: string, day: number) => {
// Option 1. Update as much of the plugin state as we can when user selects a case in CODAP. I think this might
// be too much, as it'll clear all the inputs in all the tabs and the user will have to re-enter everything
// if they were in the middle of something.
// setDayOfYear(day);
// setLatitude(_latitude);
// setLongitude(_longitude);
// ...OR...
// Option 2. Update only the day of the year, as that's reasonably unobtrusive and useful. We can first check
// if user actually selected the case from the same location, and only then update the day of the year.
if (latitude === _latitude && longitude === _longitude) {
setDayOfYear(day);
const handleCaseSelectionInCodap = useCallback((
selectedLatitude: string,
selectedLongitude: string,
selectedDay: number
) => {
const { _latitude, _longitude, _dayOfYear } = currentDayLocationRef.current;
const rowInLocation = `${_latitude},${_longitude}` === `${selectedLatitude},${selectedLongitude}`;
const newDayChoice = `${_dayOfYear}` !== `${selectedDay}`;
if (rowInLocation && newDayChoice) {
setDayOfYear(selectedDay);
currentDayLocationRef.current._dayOfYear = selectedDay;
}
}

const { getUniqueLocationsInCodapData } = useCodapData();
// Store a ref to getUniqueLocationsInCodapData so we can call inside useEffect without triggering unnecessary re-runs
const getUniqueLocationsRef = useRef(getUniqueLocationsInCodapData);
}, []);

useEffect(() => {
const initialize = async () => {
Expand All @@ -64,36 +92,66 @@ export const App: React.FC = () => {
} catch (e) {
console.error("Failed to initialize plugin, error:", e);
}

const casesDeletedFromCodapListener = async (listenerRes: ClientNotification) => {
console.log("|| listenerRes: ", listenerRes);
const { resource, values } = listenerRes;
const isResource = resource === `dataContextChangeNotice[${kDataContextName}]`;
if (!isResource && !values.result.cases) return;
const hasCasesDefined = values.result.cases !== undefined;
if (!hasCasesDefined) return
const casesDeleted = values.operation === "selectCases" && values.result.cases.length === 0 && values.result.success;

if ( casesDeleted ) {
const uniqeLocations = await getUniqueLocationsRef.current();
if (uniqeLocations) setLocations(uniqeLocations);
}
};
addDataContextChangeListener(kDataContextName, casesDeletedFromCodapListener);
};

initialize();
}, []);

const handleDataContextChange = useCallback(async (listenerRes: ClientNotification) => {
console.log("| dataContextChangeNotice: ", listenerRes);

Check warning on line 101 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / Build and Run Jest Tests

Unexpected console statement

Check warning on line 101 in src/components/App.tsx

View workflow job for this annotation

GitHub Actions / S3 Deploy

Unexpected console statement
const { resource, values } = listenerRes;
const isResource = resource === `dataContextChangeNotice[${kDataContextName}]`;
if (!isResource || !values.result.success) return;

const casesDeleted = values.operation === "selectCases" && values.result.cases.length === 0
const caseSelected = values.operation === "selectCases" && values.result.cases.length === 1;

//TODO: there is an unhandled path when we edit the location name
// we can use this to update the location name in the UI

if (casesDeleted) {
const uniqueLocations = await getUniqueLocationsRef.current();
if (uniqueLocations) setLocations(uniqueLocations);
}
else if (caseSelected) {
const parentCaseId = values.result.cases[0].parent;
const selectedDay = values.result.cases[0].values.dayOfYear;
const parentCase = await getCaseByID(kDataContextName, parentCaseId);
const selectedLatitude = parentCase.values.case.values.latitude;
const selectedLongitude = parentCase.values.case.values.longitude;
handleCaseSelectionInCodap(
selectedLatitude,
selectedLongitude,
selectedDay
);
}
}, [handleCaseSelectionInCodap]);

useEffect(() => {
addDataContextChangeListener(kDataContextName, handleDataContextChange);
}, [handleDataContextChange]);

useEffect(() => {
currentDayLocationRef.current = {
_latitude: latitude,
_longitude: longitude,
_dayOfYear: dayOfYear
};
}, [latitude, longitude, dayOfYear]);

const handleTabClick = (tab: "location" | "simulation") => {
setActiveTab(tab);
// Update dimensions of the plugin window when switching tabs.
codapInterface.sendRequest({
action: "update",
resource: "interactiveFrame",
values: {
dimensions: tab === "location" ? kInitialDimensions : kSimulationTabDimensions
}
}).then(() => {
// This brings the plugin window to the front within CODAP
selectSelf();
}).catch((error) => {
console.error("Error updating dimensions or selecting self:", error);
});
};

Expand Down
13 changes: 7 additions & 6 deletions src/components/location-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { useCodapData } from "../hooks/useCodapData";
import { kChildCollectionAttributes } from "../constants";
import { ILocation } from "../types";
import { ICodapDataContextInfo, ILocation } from "../types";
import { LocationPicker } from "./location-picker";
import { locationsEqual } from "../utils/daylight-utils";

Expand All @@ -13,12 +13,12 @@ interface LocationTabProps {
locations: ILocation[];
locationSearch: string;
selectedAttrs: string[];
dataContext: any; // TODO the type
dataContext: ICodapDataContextInfo | null;
setLatitude: (latitude: string) => void;
setLongitude: (longitude: string) => void;
setLocationSearch: (search: string) => void;
setSelectedAttributes: (attrs: string[]) => void;
setDataContext: (context: any) => void; // TODO the type
setDataContext: (context: any) => void;
setLocations: (locations: ILocation[]) => void;
}

Expand All @@ -34,6 +34,7 @@ export const LocationTab: React.FC<LocationTabProps> = ({
setSelectedAttributes,
setLocations
}) => {

const {
dataContext,
handleClearData,
Expand All @@ -43,14 +44,14 @@ export const LocationTab: React.FC<LocationTabProps> = ({
} = useCodapData();

useEffect(() => {
const updateAttributesVisibility = async () => {
const updateEachAttrVisibility = () => {
for (const attr of kChildCollectionAttributes) {
const isSelected = selectedAttrs.includes(attr.name);
await updateAttributeVisibility(attr.name, !isSelected);
updateAttributeVisibility(attr.name, !isSelected);
}
};

updateAttributesVisibility();
updateEachAttrVisibility();
}, [selectedAttrs, updateAttributeVisibility]);

const handleLatChange = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down
101 changes: 77 additions & 24 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const kDefaultMaxRows = 4;
export const kParentCollectionName = "Locations";
export const kChildCollectionName = "Daylight Info";

export const kAdjustSpringForwardOutlier = false;

export const kParentCollectionAttributes = [
{
name: "latitude",
Expand All @@ -31,67 +33,118 @@ export const kParentCollectionAttributes = [
{
name: "longitude",
type: "numeric"

},
{
name: "location",
type: "categorical"

}
// NOTE: If data are to be historical, add year attribute
];

export const kChildCollectionAttributes = [
{
name: "date",
title: "Date",
type: "date",
hasToken: true
hasToken: true,
precision: "day",
description: "Date"
},
{
name: "dayLength",
title: "Day Length",
name: "dayOfYear",
title: "Day of year",
type: "numeric",
hasToken: true
hasToken: false,
hidden: true,
description: "Day of year"
},
{
name: "sunrise",
title: "Sunrise",
name: "Day length",
title: "Day length",
type: "numeric",
hasToken: true,
unit: "hours",
description: "Day length in hours"
},
{
name: "rawSunrise",
title: "rawSunrise",
type: "date",
hasToken: true
hasToken: false,
hidden: true,
precision: "seconds",
description: "sunrise as date object"
},
{
name: "sunset",
title: "Sunset",
name: "rawSunset",
title: "rawSunset",
type: "date",
hasToken: true
hasToken: false,
hidden: true,
precision: "seconds",
description: "sunset as date object"
},
{
name: "dayNumber",
title: "Day Number",
name: "Sunrise",
title: "Sunrise",
type: "numeric",
hasToken: false
hasToken: true,
unit: "decimal hours",
formula: "hours(rawSunrise)+minutes(rawSunrise)/60",
description: "time in decimal hours"
},
{
name: "sunlightAngle",
title: "Sunlight Angle",
name: "Sunset",
title: "Sunset",
type: "numeric",
hasToken: true
hasToken: true,
unit: "decimal hours",
formula: "hours(rawSunset)+minutes(rawSunset)/60",
description: "time in decimal hours"
},
{
name: "solarIntensity",
title: "Solar Intensity",
name: "Sunlight angle",
title: "Sunlight angle",
type: "numeric",
hasToken: true
hasToken: true,
unit: "°",
description: "angle in degrees of sunlight at solar noon"
},
{
name: "Solar intensity",
title: "Solar intensity",
type: "numeric",
hasToken: true,
unit: "W/㎡",
description: "intensity of solar energy in watts per square meter at solar noon, disregarding all atmospheric effects"
},
{
name: "season",
name: "Season",
title: "Season",
type: "categorical",
hasToken: true
},
{
name: "calcId",
title: "calcId",
type: "categorical",
hasToken: false,
hidden: true,
description: "unique identifier for each location on a day - concatenation of latitude, longitude, and dayOfYear",
formula: "latitude + ',' + longitude + ',' + dayOfYear"
}
];

export const kDefaultOnAttributes = [
"date", "sunrise", "sunset", "dayLength"
"date", "Day length"
];

export const kDateWithTimeFormats = {
asZuluISO: "YYYY-MM-DDTHH:mm[Z]", // 1999-01-23T21:45Z
asLocalISOWithTZOffset: "YYYY-MM-DDTHH:mmZ", // 1999-01-23T14:45-07:00
asClockTimeString: "HH:mm", // 14:45
asClockTimeStringAMPM: "h:mm a", // 2:45 PM
}

export const kDateFormats = {
asLocalISODate: "YYYY-MM-DD",
}
Loading

0 comments on commit 8cec602

Please sign in to comment.