diff --git a/apis/datasets/v1/core.proto b/apis/datasets/v1/core.proto index 293b84e..9dd7a49 100644 --- a/apis/datasets/v1/core.proto +++ b/apis/datasets/v1/core.proto @@ -17,6 +17,7 @@ edition = "2023"; package datasets.v1; import "datasets/v1/dataset_type.proto"; +import "datasets/v1/presentation.proto"; import "tilebox/v1/id.proto"; import "tilebox/v1/query.proto"; @@ -86,6 +87,9 @@ message Dataset { bool type_editable = 13; // list of collections in the dataset repeated CollectionInfo collections = 14; + // Optional curated metadata for catalog display and client-side search/filtering, returned in list and get responses. + // Absence means no presentation metadata has been supplied, not that the dataset lacks these capabilities. + DatasetPresentation presentation = 15; } // DatasetPermission is a permission a user (as part of an org) can have on a dataset @@ -116,4 +120,6 @@ message DatasetGroup { string name = 4; // optional icon of the group string icon = 5; + // Optional short description for the group heading in the catalog. Dataset facets are not inherited from groups. + string summary = 6; } diff --git a/apis/datasets/v1/dataset_type.proto b/apis/datasets/v1/dataset_type.proto index 8fe2e3d..8c38c4d 100644 --- a/apis/datasets/v1/dataset_type.proto +++ b/apis/datasets/v1/dataset_type.proto @@ -49,8 +49,11 @@ enum FieldRole { // EnumValue is one allowed value of a custom enum. message EnumValue { - // The value users ingest, query, and receive, e.g. "L1". - string value = 1 [(buf.validate.field).string.min_len = 1]; + // The value users ingest, query, and receive, e.g. "L1", limited to 128 Unicode code points. + string value = 1 [(buf.validate.field).string = { + min_len: 1 + max_len: 128 + }]; } // FieldAnnotation contains additional information about a field in a dataset. diff --git a/apis/datasets/v1/datasets.proto b/apis/datasets/v1/datasets.proto index 2809c2b..461e803 100644 --- a/apis/datasets/v1/datasets.proto +++ b/apis/datasets/v1/datasets.proto @@ -7,6 +7,7 @@ package datasets.v1; import "buf/validate/validate.proto"; import "datasets/v1/core.proto"; import "datasets/v1/dataset_type.proto"; +import "datasets/v1/presentation.proto"; import "tilebox/v1/id.proto"; option features.field_presence = IMPLICIT; @@ -32,6 +33,8 @@ message CreateDatasetRequest { ]; // optional description of the dataset, in markdown format string description = 5 [(buf.validate.field).string.max_len = 1000000]; + // Optional metadata for catalog display and client-side discovery. + DatasetPresentation presentation = 6; } // GetDatasetRequest is the request message for the GetDataset RPC method for fetching a single dataset @@ -81,6 +84,8 @@ message UpdateDatasetRequest { features.field_presence = EXPLICIT, (buf.validate.field).string.max_len = 1000000 ]; + // Replaces the entire presentation when supplied. Omit to leave unchanged; send an empty message to clear it. + DatasetPresentation presentation = 7 [features.field_presence = EXPLICIT]; } // DeleteDatasetRequest is used to delete a dataset message DeleteDatasetRequest { diff --git a/apis/datasets/v1/presentation.proto b/apis/datasets/v1/presentation.proto new file mode 100644 index 0000000..6a06d25 --- /dev/null +++ b/apis/datasets/v1/presentation.proto @@ -0,0 +1,77 @@ +// Curated metadata for dataset display and client-side discovery. + +edition = "2023"; + +package datasets.v1; + +import "buf/validate/validate.proto"; + +option features.field_presence = IMPLICIT; + +// DatasetPresentation summarizes the whole dataset, without collection or asset scoping. +// Filter values mean the dataset contains these capabilities somewhere, not necessarily together in one product. +// Name, slug, summary, documentation, visibility, and temporal availability remain on Dataset and its collections. +// Missing metadata means unknown/not specified, not confirmed absence. +message DatasetPresentation { + string image_url = 1; + string image_alt = 2; + // e.g. "Optical", "Radar", "Hyperspectral", "Atmosphere". + DatasetPresentationFacet data_type = 3; + // Spectral capabilities, e.g. red, green, blue, red-edge, NIR, SWIR. + // A hyperspectral label may summarize wavelengths and channel counts instead of listing the filter values. + DatasetPresentationFacet bands = 4; + // Delivered products, variables, and quality layers, e.g. NDVI, surface temperature, methane, cloud masks. + DatasetPresentationFacet products = 5; + DatasetPresentationResolution resolution = 6; + // Values: Global, Selected areas, Regional. The label can qualify these, e.g. "Global — Land only". + DatasetPresentationFacet geographic_coverage = 7; + // Exact science-asset formats, e.g. COG, GeoTIFF, JPEG2000, SAFE, HDF5; excludes preview formats. + DatasetPresentationFacet asset_formats = 8; + // Source storage access, independent of Tilebox authentication and dataset visibility. + // Values: No account required, Free account required, Requester pays, Restricted. + // No account required includes freely obtainable tokens, such as Planetary Computer SAS signing. + DatasetPresentationFacet storage_access = 9; +} + +// DatasetPresentationFacet combines a compact row summary with independently selectable filter values. +// Values are both filter labels and exact, case-sensitive identities within the facet. Publishers use consistent +// spelling across datasets. Clients deduplicate these strings for dropdowns and slugify them only for URLs. +// Search can include both the row label and values. Bands and products share an All/Any selection +// across both sets. Other facets match any selected value; combine those filters with AND at dataset level. +message DatasetPresentationFacet { + // Optional row text, e.g. "RGB · NIR" or "Visible + near-infrared · 23 or 31 bands". + // When unset or empty, display values joined with " · " in their supplied order. + // Nonempty labels must be single-line text without surrounding whitespace or control characters. + string label = 1 [(buf.validate.field).string.pattern = "^$|^[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}\\p{Zl}\\p{Zp}]*[^\\p{Z}\\p{Cc}])?$"]; + // Optional dataset-specific explanation for the row. Plain text, not markup. + string tooltip = 2; + // e.g. ["Red", "Green", "Blue"] or ["Methane (CH₄)"]. Never derive these from the row label. + // Values must be nonempty single-line text without surrounding whitespace or control characters. + repeated string values = 3 [(buf.validate.field).repeated = { + unique: true + items: { + string: {pattern: "^[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}\\p{Zl}\\p{Zp}]*[^\\p{Z}\\p{Cc}])?$"} + } + }]; +} + +// DatasetPresentationResolution uses numeric filter values rather than named options. +message DatasetPresentationResolution { + // Optional row text, e.g. "10–60 m" or "250 m–1 km". Clients may format the values when omitted. + // Nonempty labels must be single-line text without surrounding whitespace or control characters. + string label = 1 [(buf.validate.field).string.pattern = "^$|^[^\\p{Z}\\p{Cc}](?:[^\\p{Cc}\\p{Zl}\\p{Zp}]*[^\\p{Z}\\p{Cc}])?$"]; + // Optional explanation of the resolution basis, e.g. nominal GSD versus radar spatial resolution. + string tooltip = 2; + // Discrete nominal resolutions in metres, not a min/max interval: e.g. [10,20,60] or [250,500,1000]. + // Clients derive five bins: (0,1), [1,10), [10,100), [100,1000), [1000,+infinity). + // Match when any value falls in a selected bin. Do not infer values from the display label. + repeated double meters = 3 [(buf.validate.field).repeated = { + unique: true + items: { + double: { + gt: 0 + finite: true + } + } + }]; +}