Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions scripts/fetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ const query = gql`
totalArea
bbox
}
aoiGeometryInputAsset {
id
fileSize
file {
name
url
}
mimetype
}
}
totalCount
}
Expand Down
27 changes: 27 additions & 0 deletions src/components/Link/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ interface Props extends Omit<NextLinkProps, 'locale'> {
target?: string;
variant?: Variant;
title?: React.ReactNode;
download?: boolean;
// NOTE: For client-side downloads (e.g. a data: URL) where there is no
// media server to set Content-Disposition. Sets the HTML download
// attribute so the browser saves the file with this name.
downloadFileName?: string;
}

// NOTE: this does not support relative links
Expand All @@ -34,6 +39,8 @@ function Link(props: Props) {
variant = 'transparent',
className,
title,
download = false,
downloadFileName,
...rest
} = props;

Expand All @@ -56,6 +63,23 @@ function Link(props: Props) {
}
}

let { target, rel } = rest;
if (download) {
// NOTE: the server is configured to set the Content-Disposition
// header for media files when treat_as_download is set, which
// makes the browser download the file instead of opening it.
// NOTE: media URLs do not have query params so we can safely
// append the query param with `?`
if (typeof href === 'string') {
href = `${href}?treat_as_download=true`;
}
// NOTE: fallback for when the server does not handle
// treat_as_download: open the file in a new tab instead of
// navigating away from the page
target = target ?? '_blank';
rel = rel ?? 'noopener noreferrer';
}

return (
<NextLink
className={_cs(
Expand All @@ -65,8 +89,11 @@ function Link(props: Props) {
)}
// eslint-disable-next-line
{...rest}
target={target}
rel={rel}
title={title ? String(title) : undefined}
href={href}
download={downloadFileName}
>
{children}
</NextLink>
Expand Down
1 change: 1 addition & 0 deletions src/pages/[locale]/data/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ function Data(props: Props) {
href={asset.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down
121 changes: 54 additions & 67 deletions src/pages/[locale]/projects/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, {
useCallback,
useEffect,
useState,
useMemo,
Expand Down Expand Up @@ -27,7 +26,6 @@ import OgMeta from 'components/OgMeta';
import Page from 'components/Page';
import ProjectTypeIcon from 'components/ProjectTypeIcon';
import ImageWrapper from 'components/ImageWrapper';
import Button from 'components/Button';
import Hero from 'components/Hero';
import Tag from 'components/Tag';
import Card from 'components/Card';
Expand Down Expand Up @@ -120,6 +118,7 @@ type Props = {
exportGroups: UrlInfo;
exportHistory: UrlInfo;
exportAreaOfInterest: UrlInfo;
aoiGeometryInputAsset: UrlInfo;
exportResults: UrlInfo;
exportTasks: UrlInfo;
exportUsers: UrlInfo;
Expand All @@ -144,6 +143,7 @@ function Project(props: Props) {
exportGroups,
exportHistory,
exportAreaOfInterest,
aoiGeometryInputAsset,
exportResults,
exportTasks,
exportUsers,
Expand Down Expand Up @@ -338,30 +338,40 @@ function Project(props: Props) {
transformAoiToGeoJson(aoiGeometry)
), [aoiGeometry]);

const aoiGeometryBlob = useMemo(() => {
if (!aoiGeometryFeature) {
return undefined;
// NOTE: The AOI download is resolved from a priority chain:
// 1. aoiGeometryInputAsset (the original uploaded geometry; not present
// for projects sourced from a tasking manager id)
// 2. exportAreaOfInterest (the generated AOI export file; also covers
// older projects)
// 3. aoiGeometry.bbox (last resort, built client-side as a data URL)
const aoiDownload = useMemo(() => {
if (aoiGeometryInputAsset?.file?.url) {
return {
kind: 'link' as const,
url: aoiGeometryInputAsset.file.url,
mimetype: aoiGeometryInputAsset.mimetype,
fileSize: aoiGeometryInputAsset.fileSize,
};
}
const blob = new Blob(
[JSON.stringify(aoiGeometryFeature, null, 2)],
{ type: 'application/geo+json' },
);
return blob;
}, [aoiGeometryFeature]);

const onAoiDownloadClick = useCallback(() => {
if (!aoiGeometryBlob) {
return;
if (exportAreaOfInterest?.file?.url) {
return {
kind: 'link' as const,
url: exportAreaOfInterest.file.url,
mimetype: exportAreaOfInterest.mimetype,
fileSize: exportAreaOfInterest.fileSize,
};
}
const url = URL.createObjectURL(aoiGeometryBlob);

const a = document.createElement('a');
a.href = url;
a.download = `area_of_interest_${firebaseId}.geojson`;
a.click();

URL.revokeObjectURL(url);
}, [aoiGeometryBlob, firebaseId]);
if (aoiGeometryFeature) {
const geojson = JSON.stringify(aoiGeometryFeature);
return {
kind: 'data' as const,
url: `data:application/geo+json;charset=utf-8,${encodeURIComponent(geojson)}`,
mimetype: 'GEOJSON',
fileSize: new TextEncoder().encode(geojson).length,
};
}
return undefined;
}, [aoiGeometryInputAsset, exportAreaOfInterest, aoiGeometryFeature]);

return (
<Page contentClassName={_cs(styles.project, className)}>
Expand Down Expand Up @@ -632,6 +642,7 @@ function Project(props: Props) {
href={exportAggregatedResults?.file?.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -662,6 +673,7 @@ function Project(props: Props) {
href={exportAggregatedResultsWithGeometry?.file?.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -690,6 +702,7 @@ function Project(props: Props) {
href={exportGroups?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -718,6 +731,7 @@ function Project(props: Props) {
href={exportHistory?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -746,6 +760,7 @@ function Project(props: Props) {
href={exportResults?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -774,6 +789,7 @@ function Project(props: Props) {
href={exportTasks?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -802,73 +818,42 @@ function Project(props: Props) {
href={exportUsers?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
</Link>
</Card>
)}
{aoiGeometry && (
<Card
childrenContainerClassName={styles.downloadCard}
heading={t('area-of-interest-title')}
description={t('area-of-interest-description')}
>
<div className={styles.fileDetails}>
<Tag>GEOJSON</Tag>
<div>
{t('download-size', {
size: getFileSizeProperties(
aoiGeometryBlob?.size ?? 0,
).size,
formatParams: {
size: {
style: 'unit',
unit: getFileSizeProperties(
aoiGeometryBlob?.size ?? 0,
).unit,
maximumFractionDigits: 1,
},
},
})}
</div>
</div>
<Button
variant="border"
className={styles.link}
onClick={onAoiDownloadClick}
>
<IoDownloadOutline />
{t('download')}
</Button>
</Card>
)}
{/* NOTE: If in case there is no aoiGeometry,
we show exportAreaOfInterest if present */}
{!aoiGeometry && exportAreaOfInterest && (
{aoiDownload && (
<Card
childrenContainerClassName={styles.downloadCard}
key={exportAreaOfInterest?.id}
heading={t('area-of-interest-title')}
description={t('area-of-interest-description')}
>
<div className={styles.fileDetails}>
<Tag>
{exportAreaOfInterest?.mimetype}
{aoiDownload.mimetype}
</Tag>
<div>
{t('download-size', {
size: getFileSizeProperties(
exportAreaOfInterest?.fileSize,
aoiDownload.fileSize,
).size,
formatParams: { size: { style: 'unit', unit: getFileSizeProperties(exportAreaOfInterest?.fileSize).unit, maximumFractionDigits: 1 } },
formatParams: { size: { style: 'unit', unit: getFileSizeProperties(aoiDownload.fileSize).unit, maximumFractionDigits: 1 } },
})}
</div>
</div>
<Link
href={exportAreaOfInterest?.file.url}
href={aoiDownload.url}
variant="buttonTransparent"
className={styles.link}
download={aoiDownload.kind === 'link'}
downloadFileName={
aoiDownload.kind === 'data'
? `area_of_interest_${firebaseId}.geojson`
: undefined
}
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -899,6 +884,7 @@ function Project(props: Props) {
href={exportHotTaskingManagerGeometries?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down Expand Up @@ -929,6 +915,7 @@ function Project(props: Props) {
href={exportModerateToHighAgreementYesMaybeGeometries?.file.url}
variant="buttonTransparent"
className={styles.link}
download
>
<IoDownloadOutline />
{t('download')}
Expand Down
Loading