From 8745fddfbcbe6f179cdf06dab31ef80d50a81184 Mon Sep 17 00:00:00 2001 From: Chad Burt Date: Fri, 2 Feb 2024 15:45:45 -0800 Subject: [PATCH] Data downloads! --- packages/api/generated-schema-clean.gql | 25 ++ packages/api/generated-schema.gql | 25 ++ packages/api/migrations/committed/000292.sql | 41 ++ packages/api/migrations/committed/000293.sql | 349 ++++++++++++++++++ packages/api/schema.sql | 79 ++++ .../client/src/admin/data/DataSettings.tsx | 85 +++-- .../data/LayerTableOfContentsItemEditor.tsx | 9 +- .../admin/data/TableOfContentsItemMenu.tsx | 33 +- .../src/dataLayers/DataDownloadModal.tsx | 249 +++++++++++++ packages/client/src/generated/graphql.ts | 116 +++++- packages/client/src/generated/queries.ts | 88 ++++- packages/client/src/projects/ProjectApp.tsx | 311 ++++++++-------- .../queries/PublishedTableOfContents.graphql | 25 ++ 13 files changed, 1231 insertions(+), 204 deletions(-) create mode 100644 packages/api/migrations/committed/000292.sql create mode 100644 packages/api/migrations/committed/000293.sql create mode 100644 packages/client/src/dataLayers/DataDownloadModal.tsx diff --git a/packages/api/generated-schema-clean.gql b/packages/api/generated-schema-clean.gql index f6c8fdee..f93b4bd0 100644 --- a/packages/api/generated-schema-clean.gql +++ b/packages/api/generated-schema-clean.gql @@ -3053,6 +3053,15 @@ enum DataSourceTypes { VIDEO } +enum DataUploadOutputType { + FLAT_GEOBUF + GEO_JSON + GEO_TIFF + PMTILES + PNG + ZIPPED_SHAPEFILE +} + enum DataUploadState { AWAITING_UPLOAD CARTOGRAPHY @@ -4647,6 +4656,13 @@ type DismissFailedUploadPayload { query: Query } +type DownloadOption { + isOriginal: Boolean + size: BigInt + type: DataUploadOutputType + url: String +} + scalar Email """ @@ -13218,6 +13234,15 @@ type TableOfContentsItem implements Node { If is_folder=false, a DataLayers visibility will be controlled by this item """ dataLayerId: Int + + """Reads and enables pagination through a set of `DownloadOption`.""" + downloadOptions( + """Only read the first `n` values of the set.""" + first: Int + + """Skip the first `n` values.""" + offset: Int + ): [DownloadOption!] enableDownload: Boolean! ftsAr: String ftsDa: String diff --git a/packages/api/generated-schema.gql b/packages/api/generated-schema.gql index f6c8fdee..f93b4bd0 100644 --- a/packages/api/generated-schema.gql +++ b/packages/api/generated-schema.gql @@ -3053,6 +3053,15 @@ enum DataSourceTypes { VIDEO } +enum DataUploadOutputType { + FLAT_GEOBUF + GEO_JSON + GEO_TIFF + PMTILES + PNG + ZIPPED_SHAPEFILE +} + enum DataUploadState { AWAITING_UPLOAD CARTOGRAPHY @@ -4647,6 +4656,13 @@ type DismissFailedUploadPayload { query: Query } +type DownloadOption { + isOriginal: Boolean + size: BigInt + type: DataUploadOutputType + url: String +} + scalar Email """ @@ -13218,6 +13234,15 @@ type TableOfContentsItem implements Node { If is_folder=false, a DataLayers visibility will be controlled by this item """ dataLayerId: Int + + """Reads and enables pagination through a set of `DownloadOption`.""" + downloadOptions( + """Only read the first `n` values of the set.""" + first: Int + + """Skip the first `n` values.""" + offset: Int + ): [DownloadOption!] enableDownload: Boolean! ftsAr: String ftsDa: String diff --git a/packages/api/migrations/committed/000292.sql b/packages/api/migrations/committed/000292.sql new file mode 100644 index 00000000..52f60624 --- /dev/null +++ b/packages/api/migrations/committed/000292.sql @@ -0,0 +1,41 @@ +--! Previous: sha1:d70db66b6510ada0a863d3d17bb89203fd3c8036 +--! Hash: sha1:dd418c91588b73943d5eabd3430341febd583459 + +-- Enter migration here + +drop function if exists table_of_contents_items_download_options(item table_of_contents_items) cascade; +drop type if exists download_option cascade; +create type download_option as ( + type data_upload_output_type, + url text, + is_original boolean, + size bigint +); + +create or replace function table_of_contents_items_download_options(item table_of_contents_items) + returns setof download_option + language sql + stable + security definer + as $$ + select + type, + (case when is_original then + replace(url, 'tiles.seasketch.org', 'uploads.seasketch.org') || '?download=' || original_filename + else + replace(url, 'tiles.seasketch.org', 'uploads.seasketch.org') || '?download=' || substring(original_filename from '(.*)\.\w+$') || substring(url from '.*(\.\w+)$') + end) as url, + is_original, + size + from + data_upload_outputs + where + data_upload_outputs.data_source_id = ( + select data_source_id from data_layers where data_layers.id = item.data_layer_id + ); + $$; + +grant execute on function table_of_contents_items_download_options(table_of_contents_items) to anon; + + +comment on function table_of_contents_items_download_options(table_of_contents_items) is '@simpleCollections only'; diff --git a/packages/api/migrations/committed/000293.sql b/packages/api/migrations/committed/000293.sql new file mode 100644 index 00000000..25f6cb9d --- /dev/null +++ b/packages/api/migrations/committed/000293.sql @@ -0,0 +1,349 @@ +--! Previous: sha1:dd418c91588b73943d5eabd3430341febd583459 +--! Hash: sha1:3b30fee54a49de60a48d4e3d11cc2e3535afac04 + +-- Enter migration here + +CREATE OR REPLACE FUNCTION public.publish_table_of_contents("projectId" integer) RETURNS SETOF public.table_of_contents_items + LANGUAGE plpgsql SECURITY DEFINER + AS $$ + declare + lid int; + item table_of_contents_items; + source_id int; + copied_source_id int; + acl_type access_control_list_type; + acl_id int; + orig_acl_id int; + new_toc_id int; + new_interactivity_settings_id int; + begin + -- check permissions + if session_is_admin("projectId") = false then + raise 'Permission denied. Must be a project admin'; + end if; + -- delete existing published table of contents items, layers, sources, and interactivity settings + delete from + interactivity_settings + where + id in ( + select + data_layers.interactivity_settings_id + from + data_layers + inner JOIN + table_of_contents_items + on + data_layers.id = table_of_contents_items.data_layer_id + where + table_of_contents_items.project_id = "projectId" and + is_draft = false + ); + + delete from data_sources where data_sources.id in ( + select + data_source_id + from + data_layers + inner JOIN + table_of_contents_items + on + data_layers.id = table_of_contents_items.data_layer_id + where + table_of_contents_items.project_id = "projectId" and + is_draft = false + ); + delete from data_layers where id in ( + select + data_layer_id + from + table_of_contents_items + where + project_id = "projectId" and + is_draft = false + ); + delete from + table_of_contents_items + where + project_id = "projectId" and + is_draft = false; + + -- one-by-one, copy related layers and link table of contents items + for item in + select + * + from + table_of_contents_items + where + is_draft = true and + project_id = "projectId" + loop + if item.is_folder = false then + -- copy interactivity settings first + insert into interactivity_settings ( + type, + short_template, + long_template, + cursor, + title + ) select + type, + short_template, + long_template, + cursor, + title + from + interactivity_settings + where + interactivity_settings.id = ( + select interactivity_settings_id from data_layers where data_layers.id = item.data_layer_id + ) + returning + id + into + new_interactivity_settings_id; + + insert into data_layers ( + project_id, + data_source_id, + source_layer, + sublayer, + render_under, + mapbox_gl_styles, + interactivity_settings_id, + z_index + ) + select "projectId", + data_source_id, + source_layer, + sublayer, + render_under, + mapbox_gl_styles, + new_interactivity_settings_id, + z_index + from + data_layers + where + id = item.data_layer_id + returning id into lid; + else + lid = item.data_layer_id; + end if; + -- TODO: this will have to be modified with the addition of any columns + insert into table_of_contents_items ( + is_draft, + project_id, + path, + stable_id, + parent_stable_id, + title, + is_folder, + show_radio_children, + is_click_off_only, + metadata, + bounds, + data_layer_id, + sort_index, + hide_children, + geoprocessing_reference_id, + translated_props + ) values ( + false, + "projectId", + item.path, + item.stable_id, + item.parent_stable_id, + item.title, + item.is_folder, + item.show_radio_children, + item.is_click_off_only, + item.metadata, + item.bounds, + lid, + item.sort_index, + item.hide_children, + item.geoprocessing_reference_id, + item.translated_props + ) returning id into new_toc_id; + select + type, id into acl_type, orig_acl_id + from + access_control_lists + where + table_of_contents_item_id = ( + select + id + from + table_of_contents_items + where is_draft = true and stable_id = item.stable_id + ); + -- copy access control list settings + if acl_type != 'public' then + update + access_control_lists + set type = acl_type + where table_of_contents_item_id = new_toc_id + returning id into acl_id; + if acl_type = 'group' then + insert into + access_control_list_groups ( + access_control_list_id, + group_id + ) + select + acl_id, + group_id + from + access_control_list_groups + where + access_control_list_id = orig_acl_id; + end if; + end if; + end loop; + -- one-by-one, copy related sources and update foreign keys of layers + for source_id in + select distinct(data_source_id) from data_layers where id in ( + select + data_layer_id + from + table_of_contents_items + where + is_draft = false and + project_id = "projectId" and + is_folder = false + ) + loop + -- TODO: This function will have to be updated whenever the schema + -- changes since these columns are hard coded... no way around it. + insert into data_sources ( + project_id, + type, + attribution, + bounds, + maxzoom, + minzoom, + url, + scheme, + tiles, + tile_size, + encoding, + buffer, + cluster, + cluster_max_zoom, + cluster_properties, + cluster_radius, + generate_id, + line_metrics, + promote_id, + tolerance, + coordinates, + urls, + query_parameters, + use_device_pixel_ratio, + import_type, + original_source_url, + enhanced_security, + byte_length, + supports_dynamic_layers, + uploaded_source_filename, + uploaded_source_layername, + normalized_source_object_key, + normalized_source_bytes, + geostats, + upload_task_id, + translated_props, + arcgis_fetch_strategy + ) + select + "projectId", + type, + attribution, + bounds, + maxzoom, + minzoom, + url, + scheme, + tiles, + tile_size, + encoding, + buffer, + cluster, + cluster_max_zoom, + cluster_properties, + cluster_radius, + generate_id, + line_metrics, + promote_id, + tolerance, + coordinates, + urls, + query_parameters, + use_device_pixel_ratio, + import_type, + original_source_url, + enhanced_security, + byte_length, + supports_dynamic_layers, + uploaded_source_filename, + uploaded_source_layername, + normalized_source_object_key, + normalized_source_bytes, + geostats, + upload_task_id, + translated_props, + arcgis_fetch_strategy + from + data_sources + where + id = source_id + returning id into copied_source_id; + -- copy data_upload_outputs + insert into data_upload_outputs ( + data_source_id, + project_id, + type, + created_at, + url, + remote, + is_original, + size, + filename, + original_filename + ) select + copied_source_id, + project_id, + type, + created_at, + url, + remote, + is_original, + size, + filename, + original_filename + from + data_upload_outputs + where + data_source_id = source_id; + -- update data_layers that should now reference the copy + update + data_layers + set data_source_id = copied_source_id + where + data_source_id = source_id and + data_layers.id in (( + select distinct(data_layer_id) from table_of_contents_items where is_draft = false and + project_id = "projectId" and + is_folder = false + )); + end loop; + update + projects + set + draft_table_of_contents_has_changes = false, + table_of_contents_last_published = now() + where + id = "projectId"; + -- return items + return query select * from table_of_contents_items + where project_id = "projectId" and is_draft = false; + end; + $$; diff --git a/packages/api/schema.sql b/packages/api/schema.sql index c9fbd633..2289b859 100644 --- a/packages/api/schema.sql +++ b/packages/api/schema.sql @@ -259,6 +259,18 @@ CREATE TYPE public.data_upload_type AS ENUM ( ); +-- +-- Name: download_option; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.download_option AS ( + type public.data_upload_output_type, + url text, + is_original boolean, + size bigint +); + + -- -- Name: email; Type: DOMAIN; Schema: public; Owner: - -- @@ -11216,6 +11228,33 @@ CREATE FUNCTION public.publish_table_of_contents("projectId" integer) RETURNS SE where id = source_id returning id into copied_source_id; + -- copy data_upload_outputs + insert into data_upload_outputs ( + data_source_id, + project_id, + type, + created_at, + url, + remote, + is_original, + size, + filename, + original_filename + ) select + copied_source_id, + project_id, + type, + created_at, + url, + remote, + is_original, + size, + filename, + original_filename + from + data_upload_outputs + where + data_source_id = source_id; -- update data_layers that should now reference the copy update data_layers @@ -12918,6 +12957,38 @@ CREATE FUNCTION public.surveys_submitted_response_count(survey public.surveys) R $$; +-- +-- Name: table_of_contents_items_download_options(public.table_of_contents_items); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.table_of_contents_items_download_options(item public.table_of_contents_items) RETURNS SETOF public.download_option + LANGUAGE sql STABLE SECURITY DEFINER + AS $_$ + select + type, + (case when is_original then + replace(url, 'tiles.seasketch.org', 'uploads.seasketch.org') || '?download=' || original_filename + else + replace(url, 'tiles.seasketch.org', 'uploads.seasketch.org') || '?download=' || substring(original_filename from '(.*)\.\w+$') || substring(url from '.*(\.\w+)$') + end) as url, + is_original, + size + from + data_upload_outputs + where + data_upload_outputs.data_source_id = ( + select data_source_id from data_layers where data_layers.id = item.data_layer_id + ); + $_$; + + +-- +-- Name: FUNCTION table_of_contents_items_download_options(item public.table_of_contents_items); Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON FUNCTION public.table_of_contents_items_download_options(item public.table_of_contents_items) IS '@simpleCollections only'; + + -- -- Name: table_of_contents_items_has_metadata(public.table_of_contents_items); Type: FUNCTION; Schema: public; Owner: - -- @@ -28712,6 +28783,14 @@ REVOKE ALL ON FUNCTION public.surveys_submitted_response_count(survey public.sur GRANT ALL ON FUNCTION public.surveys_submitted_response_count(survey public.surveys) TO seasketch_user; +-- +-- Name: FUNCTION table_of_contents_items_download_options(item public.table_of_contents_items); Type: ACL; Schema: public; Owner: - +-- + +REVOKE ALL ON FUNCTION public.table_of_contents_items_download_options(item public.table_of_contents_items) FROM PUBLIC; +GRANT ALL ON FUNCTION public.table_of_contents_items_download_options(item public.table_of_contents_items) TO anon; + + -- -- Name: FUNCTION table_of_contents_items_has_metadata(toc public.table_of_contents_items); Type: ACL; Schema: public; Owner: - -- diff --git a/packages/client/src/admin/data/DataSettings.tsx b/packages/client/src/admin/data/DataSettings.tsx index 2e28acf9..a6f3c2e0 100644 --- a/packages/client/src/admin/data/DataSettings.tsx +++ b/packages/client/src/admin/data/DataSettings.tsx @@ -11,6 +11,7 @@ import Legend from "../../dataLayers/Legend"; import useCommonLegendProps from "../../dataLayers/useCommonLegendProps"; import { TableOfContentsMetadataModalProvider } from "../../dataLayers/TableOfContentsMetadataModal"; import { LayerEditingContextProvider } from "./LayerEditingContext"; +import { DataDownloadModalProvider } from "../../dataLayers/DataDownloadModal"; export default function DataSettings() { const { path } = useRouteMatch(); @@ -30,47 +31,49 @@ export default function DataSettings() { - - - -
- -
-
- {legendProps.items.length > 0 && ( - - )} - {data?.projectBySlug && ( - - )} -
-
-
-
+ + + + +
+ +
+
+ {legendProps.items.length > 0 && ( + + )} + {data?.projectBySlug && ( + + )} +
+
+
+
+
diff --git a/packages/client/src/admin/data/LayerTableOfContentsItemEditor.tsx b/packages/client/src/admin/data/LayerTableOfContentsItemEditor.tsx index 03251200..c06b101b 100644 --- a/packages/client/src/admin/data/LayerTableOfContentsItemEditor.tsx +++ b/packages/client/src/admin/data/LayerTableOfContentsItemEditor.tsx @@ -25,10 +25,7 @@ import { gql, useApolloClient } from "@apollo/client"; import useDebounce from "../../useDebounce"; import InputBlock from "../../components/InputBlock"; import GLStyleEditor from "./GLStyleEditor/GLStyleEditor"; -import { - ClipboardCopyIcon, - DotsHorizontalIcon, -} from "@heroicons/react/outline"; +import { ClipboardCopyIcon } from "@heroicons/react/outline"; import Tabs, { NonLinkTabItem } from "../../components/Tabs"; import { Tooltip, @@ -414,8 +411,8 @@ export default function LayerTableOfContentsItemEditor(

- If enabled, users will be able to download this dataset in - GeoJSON vector format using the context menu. + If enabled, users will be able to download the original data + file uploaded to SeaSketch.

diff --git a/packages/client/src/admin/data/TableOfContentsItemMenu.tsx b/packages/client/src/admin/data/TableOfContentsItemMenu.tsx index e9a2114f..afb63325 100644 --- a/packages/client/src/admin/data/TableOfContentsItemMenu.tsx +++ b/packages/client/src/admin/data/TableOfContentsItemMenu.tsx @@ -13,6 +13,10 @@ import { import { TableOfContentsMetadataModalContext } from "../../dataLayers/TableOfContentsMetadataModal"; import Skeleton from "../../components/Skeleton"; import { ArrowDownIcon, ArrowUpIcon } from "@radix-ui/react-icons"; +import { createPortal } from "react-dom"; +import DataDownloadModal, { + DataDownloadModalContext, +} from "../../dataLayers/DataDownloadModal"; const LazyAdminItems = React.lazy( () => import( @@ -37,6 +41,7 @@ export const TableOfContentsItemMenu = React.forwardRef< ) => { const MenuType = type; const mapContext = useContext(MapContext); + const dataDownloadModalContext = useContext(DataDownloadModalContext); const [opacity, setOpacity] = useState( mapContext.layerStatesByTocStaticId[items[0]?.stableId]?.opacity || 1 @@ -84,6 +89,15 @@ export const TableOfContentsItemMenu = React.forwardRef< } : {})} > + {dataDownloadModalContext.dataDownloadModal && + createPortal( + , + document.body + )} {!manager || (!mapContext.ready && ( @@ -132,6 +146,18 @@ export const TableOfContentsItemMenu = React.forwardRef< > View metadata + {firstItem.enableDownload && firstItem.primaryDownloadUrl && ( + { + dataDownloadModalContext.setDataDownloadModal( + firstItem.id + ); + }} + > + Download... + + )} ; diff --git a/packages/client/src/dataLayers/DataDownloadModal.tsx b/packages/client/src/dataLayers/DataDownloadModal.tsx new file mode 100644 index 00000000..43beaafd --- /dev/null +++ b/packages/client/src/dataLayers/DataDownloadModal.tsx @@ -0,0 +1,249 @@ +import { createContext, useMemo, useState } from "react"; +import { useGlobalErrorHandler } from "../components/GlobalErrorHandler"; +import Modal from "../components/Modal"; +import { useTranslatedProps } from "../components/TranslatedPropControl"; +import { + DataUploadOutputType, + useDataDownloadInfoQuery, +} from "../generated/graphql"; +import bytes from "bytes"; +import { Trans, useTranslation } from "react-i18next"; + +const priority = [ + DataUploadOutputType.ZippedShapefile, + DataUploadOutputType.GeoJson, + DataUploadOutputType.GeoTiff, + DataUploadOutputType.Png, + DataUploadOutputType.FlatGeobuf, + DataUploadOutputType.Pmtiles, +]; + +export const DataDownloadModalContext = createContext<{ + setDataDownloadModal: (tocId: number | undefined) => void; + dataDownloadModal?: number; +}>({ + setDataDownloadModal: () => {}, +}); + +export default function DataDownloadModal({ + onRequestClose, + tocId, +}: { + onRequestClose: () => void; + tocId: number; +}) { + const { t } = useTranslation("homepage"); + const onError = useGlobalErrorHandler(); + const { data, loading } = useDataDownloadInfoQuery({ + variables: { + tocId, + }, + onError, + }); + + const sortedOptions = useMemo(() => { + return [...(data?.tableOfContentsItem?.downloadOptions || [])] + .sort((a, b) => { + // Get the index of the types in the priority array + let indexOfA = priority.indexOf(a.type!); + let indexOfB = priority.indexOf(b.type!); + + // If one of the types is not found in the priority array, we consider its priority lowest + indexOfA = indexOfA === -1 ? priority.length : indexOfA; + indexOfB = indexOfB === -1 ? priority.length : indexOfB; + + // Compare the indices to determine order + if (indexOfA < indexOfB) { + return -1; + } + if (indexOfA > indexOfB) { + return 1; + } + // a must be equal to b + return 0; + }) + .filter((option) => !option.isOriginal); + }, [data?.tableOfContentsItem?.downloadOptions]); + + const getTranslatedProp = useTranslatedProps(data?.tableOfContentsItem); + + const original = useMemo(() => { + return data?.tableOfContentsItem?.downloadOptions?.find( + (option) => option.isOriginal + ); + }, [data?.tableOfContentsItem?.downloadOptions]); + + return ( + + {t("Download")}{" "} + {getTranslatedProp("title")} + + } + onRequestClose={onRequestClose} + loading={loading} + > +
+ {original?.url && ( +
+

+ {data?.tableOfContentsItem?.dataLayer?.dataSource?.createdAt ? ( + + {t("Original file uploaded on ")} + {new Date( + data.tableOfContentsItem.dataLayer.dataSource.createdAt + ).toLocaleDateString()} + + ) : ( + t("Original file") + )} +

+ +
+ +
+
+ )} +
+

{t("Alternate formats")}

+
    + {sortedOptions.map((option) => { + const filename = new URL( + option.url || "https://www.example.com" + ).pathname + .split("/") + .pop(); + const downloadFilename = new URL( + option.url || "https://www.example.com" + ).searchParams.get("download"); + return ( +
  • +
    + + {downloadFilename || filename} + + {bytes(parseInt(option.size) || 0)} +
    + +
  • + ); + })} +
+
+
+
+ ); +} + +export function DataDownloadModalProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [dataDownloadModal, setDataDownloadModal] = useState< + number | undefined + >(); + return ( + + {dataDownloadModal !== undefined && ( + setDataDownloadModal(undefined)} + tocId={dataDownloadModal} + /> + )} + {children} + + ); +} + +function DownloadFormatDescription({ type }: { type: DataUploadOutputType }) { + const { t } = useTranslation("homepage"); + return ( +

+ {(() => { + switch (type) { + case DataUploadOutputType.GeoJson: + return t( + "GeoJSON is an uncompressed text format compatible with most spatial software." + ); + case DataUploadOutputType.GeoTiff: + return t( + "GeoTIFF is a public domain metadata standard which allows georeferencing information to be embedded within a TIFF file." + ); + case DataUploadOutputType.Png: + return t( + "PNG is a raster-graphics file-format that supports lossless data compression. It has no spatial metadata encoded within it so is not useful in spatial software." + ); + case DataUploadOutputType.FlatGeobuf: + return ( + + + FlatGeobuf + {" "} + is a compact binary encoding for geographic data based on + flatbuffers. It is a relatively new format only compatible with + recent versions of open-source software. + + ); + case DataUploadOutputType.Pmtiles: + return ( + + The{" "} + + PMTiles + {" "} + format is an archive of all the tiles used to render this layer + in SeaSketch. + + ); + case DataUploadOutputType.ZippedShapefile: + return ( + + + Zipped shapefiles + {" "} + are a compressed binary format compatible with most software. + + ); + default: + return null; + } + })()} +

+ ); +} diff --git a/packages/client/src/generated/graphql.ts b/packages/client/src/generated/graphql.ts index 79f8d37a..aa05e7b2 100644 --- a/packages/client/src/generated/graphql.ts +++ b/packages/client/src/generated/graphql.ts @@ -2569,6 +2569,15 @@ export enum DataSourcesOrderBy { ProjectIdDesc = 'PROJECT_ID_DESC' } +export enum DataUploadOutputType { + FlatGeobuf = 'FLAT_GEOBUF', + GeoJson = 'GEO_JSON', + GeoTiff = 'GEO_TIFF', + Pmtiles = 'PMTILES', + Png = 'PNG', + ZippedShapefile = 'ZIPPED_SHAPEFILE' +} + export enum DataUploadState { AwaitingUpload = 'AWAITING_UPLOAD', Cartography = 'CARTOGRAPHY', @@ -3978,6 +3987,14 @@ export type DismissFailedUploadPayloadDataUploadTaskEdgeArgs = { orderBy?: Maybe>; }; +export type DownloadOption = { + __typename?: 'DownloadOption'; + isOriginal?: Maybe; + size?: Maybe; + type?: Maybe; + url?: Maybe; +}; + /** * Email notification preferences can be read and set by the current user session. @@ -12033,6 +12050,8 @@ export type TableOfContentsItem = Node & { dataLayer?: Maybe; /** If is_folder=false, a DataLayers visibility will be controlled by this item */ dataLayerId?: Maybe; + /** Reads and enables pagination through a set of `DownloadOption`. */ + downloadOptions?: Maybe>; enableDownload: Scalars['Boolean']; ftsAr?: Maybe; ftsDa?: Maybe; @@ -12100,6 +12119,22 @@ export type TableOfContentsItem = Node & { usesDynamicMetadata?: Maybe; }; + +/** + * TableOfContentsItems represent a tree-view of folders and operational layers + * that can be added to the map. Both layers and folders may be nested into other + * folders for organization, and each folder has its own access control list. + * + * Items that represent data layers have a `DataLayer` relation, which in turn has + * a reference to a `DataSource`. Usually these relations should be fetched in + * batch only once the layer is turned on, using the + * `dataLayersAndSourcesByLayerId` query. + */ +export type TableOfContentsItemDownloadOptionsArgs = { + first?: Maybe; + offset?: Maybe; +}; + /** * A condition to be used against `TableOfContentsItem` object types. All fields * are tested for equality and combined with a logical ‘and.’ @@ -17171,7 +17206,7 @@ export type ProjectSlugExistsQuery = ( export type OverlayFragment = ( { __typename?: 'TableOfContentsItem' } - & Pick + & Pick & { acl?: Maybe<( { __typename?: 'Acl' } & Pick @@ -17238,6 +17273,30 @@ export type SearchOverlaysQuery = ( )>> } ); +export type DataDownloadInfoQueryVariables = Exact<{ + tocId: Scalars['Int']; +}>; + + +export type DataDownloadInfoQuery = ( + { __typename?: 'Query' } + & { tableOfContentsItem?: Maybe<( + { __typename?: 'TableOfContentsItem' } + & Pick + & { downloadOptions?: Maybe + )>>, dataLayer?: Maybe<( + { __typename?: 'DataLayer' } + & Pick + & { dataSource?: Maybe<( + { __typename?: 'DataSource' } + & Pick + )> } + )> } + )> } +); + export type ProjectListItemFragment = ( { __typename?: 'Project' } & Pick @@ -20153,6 +20212,7 @@ export const OverlayFragmentDoc = gql` geoprocessingReferenceId translatedProps hasMetadata + primaryDownloadUrl } `; export const DataSourceDetailsFragmentDoc = gql` @@ -26175,6 +26235,59 @@ export function useSearchOverlaysLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt export type SearchOverlaysQueryHookResult = ReturnType; export type SearchOverlaysLazyQueryHookResult = ReturnType; export type SearchOverlaysQueryResult = Apollo.QueryResult; +export const DataDownloadInfoDocument = gql` + query DataDownloadInfo($tocId: Int!) { + tableOfContentsItem(id: $tocId) { + id + title + translatedProps + primaryDownloadUrl + downloadOptions { + url + type + isOriginal + size + } + dataLayer { + id + dataSource { + createdAt + id + type + uploadedSourceFilename + } + } + } +} + `; + +/** + * __useDataDownloadInfoQuery__ + * + * To run a query within a React component, call `useDataDownloadInfoQuery` and pass it any options that fit your needs. + * When your component renders, `useDataDownloadInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useDataDownloadInfoQuery({ + * variables: { + * tocId: // value for 'tocId' + * }, + * }); + */ +export function useDataDownloadInfoQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(DataDownloadInfoDocument, options); + } +export function useDataDownloadInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(DataDownloadInfoDocument, options); + } +export type DataDownloadInfoQueryHookResult = ReturnType; +export type DataDownloadInfoLazyQueryHookResult = ReturnType; +export type DataDownloadInfoQueryResult = Apollo.QueryResult; export const ProjectListingDocument = gql` query ProjectListing($first: Int, $after: Cursor, $last: Int, $before: Cursor) { projects: projectsConnection( @@ -30364,6 +30477,7 @@ export const namedOperations = { ProjectSlugExists: 'ProjectSlugExists', PublishedTableOfContents: 'PublishedTableOfContents', SearchOverlays: 'SearchOverlays', + DataDownloadInfo: 'DataDownloadInfo', ProjectListing: 'ProjectListing', SketchClassForm: 'SketchClassForm', TemplateSketchClasses: 'TemplateSketchClasses', diff --git a/packages/client/src/generated/queries.ts b/packages/client/src/generated/queries.ts index 3986eeed..22bf7809 100644 --- a/packages/client/src/generated/queries.ts +++ b/packages/client/src/generated/queries.ts @@ -2567,6 +2567,15 @@ export enum DataSourcesOrderBy { ProjectIdDesc = 'PROJECT_ID_DESC' } +export enum DataUploadOutputType { + FlatGeobuf = 'FLAT_GEOBUF', + GeoJson = 'GEO_JSON', + GeoTiff = 'GEO_TIFF', + Pmtiles = 'PMTILES', + Png = 'PNG', + ZippedShapefile = 'ZIPPED_SHAPEFILE' +} + export enum DataUploadState { AwaitingUpload = 'AWAITING_UPLOAD', Cartography = 'CARTOGRAPHY', @@ -3976,6 +3985,14 @@ export type DismissFailedUploadPayloadDataUploadTaskEdgeArgs = { orderBy?: Maybe>; }; +export type DownloadOption = { + __typename?: 'DownloadOption'; + isOriginal?: Maybe; + size?: Maybe; + type?: Maybe; + url?: Maybe; +}; + /** * Email notification preferences can be read and set by the current user session. @@ -12031,6 +12048,8 @@ export type TableOfContentsItem = Node & { dataLayer?: Maybe; /** If is_folder=false, a DataLayers visibility will be controlled by this item */ dataLayerId?: Maybe; + /** Reads and enables pagination through a set of `DownloadOption`. */ + downloadOptions?: Maybe>; enableDownload: Scalars['Boolean']; ftsAr?: Maybe; ftsDa?: Maybe; @@ -12098,6 +12117,22 @@ export type TableOfContentsItem = Node & { usesDynamicMetadata?: Maybe; }; + +/** + * TableOfContentsItems represent a tree-view of folders and operational layers + * that can be added to the map. Both layers and folders may be nested into other + * folders for organization, and each folder has its own access control list. + * + * Items that represent data layers have a `DataLayer` relation, which in turn has + * a reference to a `DataSource`. Usually these relations should be fetched in + * batch only once the layer is turned on, using the + * `dataLayersAndSourcesByLayerId` query. + */ +export type TableOfContentsItemDownloadOptionsArgs = { + first?: Maybe; + offset?: Maybe; +}; + /** * A condition to be used against `TableOfContentsItem` object types. All fields * are tested for equality and combined with a logical ‘and.’ @@ -17169,7 +17204,7 @@ export type ProjectSlugExistsQuery = ( export type OverlayFragment = ( { __typename?: 'TableOfContentsItem' } - & Pick + & Pick & { acl?: Maybe<( { __typename?: 'Acl' } & Pick @@ -17236,6 +17271,30 @@ export type SearchOverlaysQuery = ( )>> } ); +export type DataDownloadInfoQueryVariables = Exact<{ + tocId: Scalars['Int']; +}>; + + +export type DataDownloadInfoQuery = ( + { __typename?: 'Query' } + & { tableOfContentsItem?: Maybe<( + { __typename?: 'TableOfContentsItem' } + & Pick + & { downloadOptions?: Maybe + )>>, dataLayer?: Maybe<( + { __typename?: 'DataLayer' } + & Pick + & { dataSource?: Maybe<( + { __typename?: 'DataSource' } + & Pick + )> } + )> } + )> } +); + export type ProjectListItemFragment = ( { __typename?: 'Project' } & Pick @@ -20151,6 +20210,7 @@ export const OverlayFragmentDoc = /*#__PURE__*/ gql` geoprocessingReferenceId translatedProps hasMetadata + primaryDownloadUrl } `; export const DataSourceDetailsFragmentDoc = /*#__PURE__*/ gql` @@ -22575,6 +22635,31 @@ export const SearchOverlaysDocument = /*#__PURE__*/ gql` } } `; +export const DataDownloadInfoDocument = /*#__PURE__*/ gql` + query DataDownloadInfo($tocId: Int!) { + tableOfContentsItem(id: $tocId) { + id + title + translatedProps + primaryDownloadUrl + downloadOptions { + url + type + isOriginal + size + } + dataLayer { + id + dataSource { + createdAt + id + type + uploadedSourceFilename + } + } + } +} + `; export const ProjectListingDocument = /*#__PURE__*/ gql` query ProjectListing($first: Int, $after: Cursor, $last: Int, $before: Cursor) { projects: projectsConnection( @@ -24035,6 +24120,7 @@ export const namedOperations = { ProjectSlugExists: 'ProjectSlugExists', PublishedTableOfContents: 'PublishedTableOfContents', SearchOverlays: 'SearchOverlays', + DataDownloadInfo: 'DataDownloadInfo', ProjectListing: 'ProjectListing', SketchClassForm: 'SketchClassForm', TemplateSketchClasses: 'TemplateSketchClasses', diff --git a/packages/client/src/projects/ProjectApp.tsx b/packages/client/src/projects/ProjectApp.tsx index 7e42fd29..e59d60dc 100644 --- a/packages/client/src/projects/ProjectApp.tsx +++ b/packages/client/src/projects/ProjectApp.tsx @@ -30,6 +30,7 @@ import { import { MeasureControlContextProvider } from "../MeasureControl"; import ProjectMapLegend from "./ProjectMapLegend"; import { TableOfContentsMetadataModalProvider } from "../dataLayers/TableOfContentsMetadataModal"; +import { DataDownloadModalProvider } from "../dataLayers/DataDownloadModal"; const LazyOverlays = React.lazy( () => import(/* webpackChunkName: "Overlays" */ "./OverlayLayers") @@ -106,167 +107,169 @@ export default function ProjectApp() { - - {/* */} -
- - setExpandSidebar(false)} - mapSettingsPopupActions={ - <> - - - { - // if ( - // mapContext.digitizingLockState === - // DigitizingLockState.Free - // ) { - // mapContext.manager?.measure(); - // } else if ( - // mapContext.digitizingLockedBy === "MeasureControl" - // ) { - // mapContext.manager?.cancelMeasurement(); - // } else { - // // do nothing. Don't interfere with sketching - // } - // }} - /> - - } + + + {/* */} +
+ + setExpandSidebar(false)} + mapSettingsPopupActions={ + <> + + + { + // if ( + // mapContext.digitizingLockState === + // DigitizingLockState.Free + // ) { + // mapContext.manager?.measure(); + // } else if ( + // mapContext.digitizingLockedBy === "MeasureControl" + // ) { + // mapContext.manager?.cancelMeasurement(); + // } else { + // // do nothing. Don't interfere with sketching + // } + // }} + /> + + } + /> +
+
+ { + setExpandSidebar((prev) => !prev); + history.replace(`/${slug}/app`); + }} /> -
-
- { - setExpandSidebar((prev) => !prev); - history.replace(`/${slug}/app`); - }} - /> - - history.push(`/${slug}/app`)} - /> - - - history.replace(`/${slug}/app`)} - dark={dark} - hidden={Boolean(!showSidebar)} - noPadding={ - /sketches/.test(history.location.pathname) || - /forums/.test(history.location.pathname) || - /overlays/.test(history.location.pathname) - } - > - - - + + history.push(`/${slug}/app`)} + /> + + + history.replace(`/${slug}/app`)} + dark={dark} + hidden={Boolean(!showSidebar)} + noPadding={ + /sketches/.test(history.location.pathname) || + /forums/.test(history.location.pathname) || + /overlays/.test(history.location.pathname) } > - + + + } > - - - - - + + + + + + + { + return ( + - { - return ( - - - - - { - setExpandSidebar((prev) => false); - }} - /> - - -
+ )} + path={`/${slug}/app/sketches`} + /> + + + + + + + + { + setExpandSidebar((prev) => false); + }} + /> + + + +
diff --git a/packages/client/src/queries/PublishedTableOfContents.graphql b/packages/client/src/queries/PublishedTableOfContents.graphql index 60fbc058..be22e98a 100644 --- a/packages/client/src/queries/PublishedTableOfContents.graphql +++ b/packages/client/src/queries/PublishedTableOfContents.graphql @@ -18,6 +18,7 @@ fragment Overlay on TableOfContentsItem { geoprocessingReferenceId translatedProps hasMetadata + primaryDownloadUrl } query PublishedTableOfContents($slug: String!) { @@ -116,3 +117,27 @@ query SearchOverlays( isFolder } } + +query DataDownloadInfo($tocId: Int!) { + tableOfContentsItem(id: $tocId) { + id + title + translatedProps + primaryDownloadUrl + downloadOptions { + url + type + isOriginal + size + } + dataLayer { + id + dataSource { + createdAt + id + type + uploadedSourceFilename + } + } + } +}