Skip to content

Commit

Permalink
Merge pull request #11 from developmentseed/feature/style-and-config
Browse files Browse the repository at this point in the history
Allow config to be changed after build and some style fixes
  • Loading branch information
emmanuelmathot authored Oct 21, 2024
2 parents 3cbfcbd + 89da664 commit 98c256a
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 107 deletions.
33 changes: 31 additions & 2 deletions webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,38 @@ These files are used to simplify the configuration of the app and should not con

Run the project locally by copying the `.env` to `.env.local` and setting the following environment variables:

| | |
| ----------------- | --------------------------------------- |
| `APP_TITLE` | Application title (For meta tags) |
| `APP_DESCRIPTION` | Application description (For meta tags) |
| `PUBLIC_URL` | Full url for the app |
| `MAPBOX_TOKEN` | Mapbox token |
| `STAC_API` | STAC API endpoint |
| `TILER_API` | TILER API endpoint |

### Runtime configuration
It is possible to change some configuration after the app is built by providing the configuration via the `app_config.js` file.

The file should be placed in the root of the `dist` directory and should contain a single object:

```js
window.__APP_CONFIG__ = {
{{VARIABLE}}: {{value}}
};
```
A JSON object can also be used but needs to be converted to an object.

```js
window.__APP_CONFIG__ = JSON.parse('{{JSON_STRING}}');
```

The following variables can be changed at runtime, while the other ones are needed during the build process:
| | |
| -------------- | ------------------ |
| `MAPBOX_TOKEN` | Mapbox token |
| `STAC_API` | STAC API endpoint |
| `TILER_API` | TILER API endpoint |

| --- | --- |
| `{{VARIABLE}}` | {{description}} |

### Starting the app

Expand Down
56 changes: 25 additions & 31 deletions webapp/app/components/aois/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import Map, { Layer, Source } from 'react-map-gl';
import { CollecticonIsoStack } from '@devseed-ui/collecticons-chakra';
import debounce from 'lodash.debounce';
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
import { add, eachDayOfInterval, isWithinInterval } from 'date-fns';
import { add, eachDayOfInterval } from 'date-fns';
import { format } from 'date-fns/format.js';

import { useSettings } from './settings';
Expand All @@ -32,12 +32,12 @@ import { DataIndicator, IndicatorLegend } from './data-indicator';
import { PointStats } from './point-stats';
import { ShareOptions } from './share-options';

import { AreaTitle } from '$components/common/area-title';
import config from '$utils/config';
import {
IndicatorProperties,
FeatureProperties,
IndicatorDataRaw
FeatureProperties
} from '$utils/loaders';
import { AreaTitle } from '$components/common/area-title';
import { FloatBox } from '$components/common/shared';
import { Timeline } from '$components/common/timeline';
import { DatePicker } from '$components/common/calendar';
Expand Down Expand Up @@ -73,7 +73,6 @@ const dataFetcher = new DataFetcher<CogStatistics>();
interface LakesLoaderData {
lake: Feature<MultiPolygon, FeatureProperties>;
indicators: IndicatorProperties[];
indicatorData: IndicatorDataRaw[];
}

export function Component() {
Expand Down Expand Up @@ -105,11 +104,10 @@ export function Component() {
}
>
<Await resolve={promise.data}>
{({ lake, indicators, indicatorData }) => (
{({ lake, indicators }) => (
<LakesSingle
lake={lake}
indicators={indicators}
indicatorData={indicatorData}
/>
)}
</Await>
Expand All @@ -120,7 +118,7 @@ export function Component() {
Component.displayName = 'LakesComponent';

function LakesSingle(props: LakesLoaderData) {
const { lake, indicators, indicatorData } = props;
const { lake, indicators } = props;

const [params, setSearchParams] = useSearchParams();
const ind = params.get('ind');
Expand Down Expand Up @@ -176,7 +174,7 @@ function LakesSingle(props: LakesLoaderData) {
daysToRequest.forEach((day) => {
dataFetcher.fetchData({
key: ['lakes', lake.properties.idhidro, day.toISOString()],
url: `${process.env.STAC_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${lake.properties.idhidro}_${format(day, 'yyyyMMdd')}`
url: `${config.STAC_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${lake.properties.idhidro}_${format(day, 'yyyyMMdd')}`
});
});
}, 500),
Expand Down Expand Up @@ -261,7 +259,7 @@ function LakesSingle(props: LakesLoaderData) {
[lake]
);

const lakeIndicatorTileUrl = `${process.env.TILER_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${itemId}/tiles/{z}/{x}/{y}?assets=${activeIndicator.id}&rescale=${[valueMin, valueMax].join(',')}&colormap_name=${colorName}`;
const lakeIndicatorTileUrl = `${config.TILER_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${itemId}/tiles/{z}/{x}/{y}?assets=${activeIndicator.id}&rescale=${[valueMin, valueMax].join(',')}&colormap_name=${colorName}`;

return (
<>
Expand All @@ -284,10 +282,7 @@ function LakesSingle(props: LakesLoaderData) {
pointerEvents='all'
/>
<Divider orientation='vertical' />
<ShareOptions
indicatorData={indicatorData}
tileEndpoint={lakeIndicatorTileUrl}
/>
<ShareOptions tileEndpoint={lakeIndicatorTileUrl} />
</>
)}
/>
Expand All @@ -301,7 +296,7 @@ function LakesSingle(props: LakesLoaderData) {
onOptionChange={onMapOptionChange}
/>
<Map
mapboxAccessToken={process.env.MAPBOX_TOKEN}
mapboxAccessToken={config.MAPBOX_TOKEN}
initialViewState={{
bounds: lake.bbox as LngLatBoundsLike,
fitBoundsOptions: {
Expand Down Expand Up @@ -336,7 +331,7 @@ function LakesSingle(props: LakesLoaderData) {
<Source
type='raster'
tiles={[
`${process.env.TILER_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${itemId}/tiles/{z}/{x}/{y}?assets=TCI`
`${config.TILER_API}/collections/whis-lakes-labelec-scenes-c2rcc/items/${itemId}/tiles/{z}/{x}/{y}?assets=TCI`
]}
>
<Layer
Expand Down Expand Up @@ -384,21 +379,20 @@ function LakesSingle(props: LakesLoaderData) {
maxDate={domain[1]}
selectedDay={selectedDay}
onDaySelect={setSelectedDay}
getAllowedDays={useCallback(
({ firstDay, lastDay }) => {
const days = indicatorData
.filter((d) =>
isWithinInterval(d.date, {
start: firstDay,
end: lastDay
})
)
.map((d) => d.date);

return Promise.resolve(days);
},
[indicatorData]
)}
// getAllowedDays={useCallback(
// ({ firstDay, lastDay }) => {
// const days = indicatorData
// .filter((d) =>
// isWithinInterval(d.date, {
// start: firstDay,
// end: lastDay
// })
// )
// .map((d) => d.date);
// return Promise.resolve(days);
// },
// []
// )}
/>
<Box>
<Slider
Expand Down
3 changes: 3 additions & 0 deletions webapp/app/components/aois/map-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function MapOptionsDrawer(props: MapOptionsDrawerProps) {
True color image
</FormLabel>
<Switch
colorScheme='base'
id='true-color'
isChecked={optTrueColor}
onChange={() => {
Expand All @@ -86,6 +87,7 @@ export function MapOptionsDrawer(props: MapOptionsDrawerProps) {
Processed image
</FormLabel>
<Switch
colorScheme='base'
id='process-color'
isChecked={optProcessed}
onChange={() => {
Expand All @@ -104,6 +106,7 @@ export function MapOptionsDrawer(props: MapOptionsDrawerProps) {
Terrain elevation
</FormLabel>
<Switch
colorScheme='base'
id='terrain'
isChecked={optTerrain}
onChange={() => {
Expand Down
8 changes: 5 additions & 3 deletions webapp/app/components/aois/point-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { format } from 'date-fns/format.js';

import { PointStatsPopover } from './point-stats-popover';

import config from '$utils/config';

const markerPulse = keyframes`
0% {
opacity: 0;
Expand Down Expand Up @@ -54,7 +56,7 @@ export function PointStats(props: PointStatsProps) {
setData({ state: 'loading', data: null });
const cogUrl = getCogUrl(lakeId, indicatorId, date);
const stats = await axios.get(
`${process.env.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}`
`${config.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}`
);
const prevMeasurement = await searchPrevMeasurement(
lakeId,
Expand Down Expand Up @@ -163,7 +165,7 @@ async function searchPrevMeasurement(
};

const candidatesData = await axios.post(
`${process.env.STAC_API}/search`,
`${config.STAC_API}/search`,
candidateQuery
);

Expand All @@ -176,7 +178,7 @@ async function searchPrevMeasurement(
const cogUrl = getCogUrl(lakeId, indicatorId, date);
try {
const stats = await axios.get(
`${process.env.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}`
`${config.TILER_API}/cog/point/${lngLat?.join(',')}?url=${cogUrl}`
);
return {
date,
Expand Down
112 changes: 52 additions & 60 deletions webapp/app/components/aois/share-options.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import {
Button,
// Button,
Flex,
Heading,
IconButton,
Expand All @@ -19,72 +19,71 @@ import {
} from '@devseed-ui/collecticons-chakra';

import { CopyField } from '$components/common/copy-field';
import { IndicatorDataRaw } from '$utils/loaders';
// import { IndicatorDataRaw } from '$utils/loaders';

const indicatorData2CSV = (data) => {
const columns = [
{ id: 'date', fn: (d) => d.date.toISOString() },
'percent_valid_in_water_body',
'chlorophyll.mean',
'chlorophyll.stddev',
'chlorophyll.minimum',
'chlorophyll.maximum',
'tsm.mean',
'tsm.stddev',
'tsm.minimum',
'tsm.maximum'
];
// const indicatorData2CSV = (data) => {
// const columns = [
// { id: 'date', fn: (d) => d.date.toISOString() },
// 'percent_valid_in_water_body',
// 'chlorophyll.mean',
// 'chlorophyll.stddev',
// 'chlorophyll.minimum',
// 'chlorophyll.maximum',
// 'tsm.mean',
// 'tsm.stddev',
// 'tsm.minimum',
// 'tsm.maximum'
// ];

const header = columns
.map((c) => (typeof c === 'string' ? c : c.id))
.join(',');
// const header = columns
// .map((c) => (typeof c === 'string' ? c : c.id))
// .join(',');

const rows = data.map((row) => {
const values = columns.map((col) => {
const getter =
typeof col === 'string'
? // quick get value from nested object
(r) => col.split('.').reduce((acc, segment) => acc[segment], r)
: col.fn;
return getter(row);
});
return values.join(',');
});
// const rows = data.map((row) => {
// const values = columns.map((col) => {
// const getter =
// typeof col === 'string'
// ? // quick get value from nested object
// (r) => col.split('.').reduce((acc, segment) => acc[segment], r)
// : col.fn;
// return getter(row);
// });
// return values.join(',');
// });

const csv = `${header}\n${rows.join('\n')}`;
return csv;
};
// const csv = `${header}\n${rows.join('\n')}`;
// return csv;
// };

// Function to handle the download action
const handleDownload = (data) => {
// Generate CSV content from data
const csvContent = indicatorData2CSV(data);
// // Function to handle the download action
// const handleDownload = (data) => {
// // Generate CSV content from data
// const csvContent = indicatorData2CSV(data);

// Convert the CSV content to a Blob
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
// // Convert the CSV content to a Blob
// const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });

// Create a URL for the Blob
const url = URL.createObjectURL(blob);
// // Create a URL for the Blob
// const url = URL.createObjectURL(blob);

// Create an anchor element and trigger the download
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'data.csv'); // Name the file here
document.body.appendChild(link);
link.click();
// // Create an anchor element and trigger the download
// const link = document.createElement('a');
// link.href = url;
// link.setAttribute('download', 'data.csv'); // Name the file here
// document.body.appendChild(link);
// link.click();

// Clean up by revoking the Blob URL and removing the anchor element
URL.revokeObjectURL(url);
link.remove();
};
// // Clean up by revoking the Blob URL and removing the anchor element
// URL.revokeObjectURL(url);
// link.remove();
// };

interface ShareOptionsProps {
indicatorData: IndicatorDataRaw[];
tileEndpoint: string;
}

export function ShareOptions(props: ShareOptionsProps) {
const { indicatorData, tileEndpoint } = props;
const { tileEndpoint } = props;

return (
<Popover placement='bottom-end'>
Expand All @@ -93,6 +92,7 @@ export function ShareOptions(props: ShareOptionsProps) {
aria-label='Indicator settings'
size='xs'
variant='ghost'
colorScheme='base'
icon={<CollecticonShare />}
ml='auto'
/>
Expand All @@ -109,14 +109,6 @@ export function ShareOptions(props: ShareOptionsProps) {
>
Share
</Text>
<Flex gap={2} direction='column'>
<Heading size='xs' as='p'>
Export
</Heading>
<Button size='sm' onClick={() => handleDownload(indicatorData)}>
Timeline Data (CSV)
</Button>
</Flex>
<Flex gap={2} direction='column'>
<Heading size='xs' as='p'>
Tile endpoint
Expand Down
Loading

0 comments on commit 98c256a

Please sign in to comment.