diff --git a/CHANGES.md b/CHANGES.md index d0e581737e8..8cffe64b10c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ - Explicitly set prettier tab-width - Move release guide from README.md to RELEASE_GUIDE.md +- WMTS read URL from operations metadata - [The next improvement] #### 8.7.10 - 2024-11-29 diff --git a/lib/Models/Catalog/Ows/OwsInterfaces.ts b/lib/Models/Catalog/Ows/OwsInterfaces.ts index 1e6bbd5e21f..d9e08bdb695 100644 --- a/lib/Models/Catalog/Ows/OwsInterfaces.ts +++ b/lib/Models/Catalog/Ows/OwsInterfaces.ts @@ -6,6 +6,85 @@ export interface OnlineResource { "xlink:href": string; } +type RangeClosureType = "closed" | "open" | "open-closed" | "closed-open"; + +/** A range of values of a numeric parameter. This range can be continuous or discrete, defined by a fixed spacing between adjacent valid values. If the MinimumValue or MaximumValue is not included, there is no value limit in that direction. Inclusion of the specified minimum and maximum values in the range shall be defined by the rangeClosure. */ +export interface RangeType { + /** Specifies which of the minimum and maximum values are included in the range. Note that plus and minus infinity are considered closed bounds. */ + readonly rangeClosure?: RangeClosureType; + + /** Maximum value of this numeric parameter. */ + readonly MaximumValue?: string; + /** Minimum value of this numeric parameter. */ + readonly MinimumValue?: string; + /** The regular distance or spacing between the allowed values in a range. */ + readonly Spacing?: string; +} + +interface AllowedValuesType { + readonly Range?: RangeType | RangeType[]; + readonly Value?: string | string[]; +} + +export interface DomainType { + /** Name or identifier of this quantity. */ + name: string; + /** List of all the valid values and/or ranges of values for this quantity. For numeric quantities, signed values should be ordered from negative infinity to positive infinity. */ + readonly AllowedValues: AllowedValuesType; +} + +export interface RequestMethodType extends OnlineResource { + /** Optional unordered list of valid domain constraints on non-parameter quantities that each apply to this request method for this operation. If one of these Constraint elements has the same "name" attribute as a Constraint element in the OperationsMetadata or Operation element, this Constraint element shall override the other one for this operation. The list of required and optional constraints for this request method for this operation shall be specified in the Implementation Specification for this service. */ + readonly Constraint?: DomainType | DomainType[]; +} + +interface HTTPType { + /** Connect point URL prefix and any constraints for the HTTP "Get" request method for this operation request. */ + readonly Get: RequestMethodType | RequestMethodType[]; + /** Connect point URL and any constraints for the HTTP "Post" request method for this operation request. */ + readonly Post: RequestMethodType | RequestMethodType[]; +} + +interface DCPType { + /** Connect point URLs for the HTTP Distributed Computing Platform (DCP). Normally, only one Get and/or one Post is included in this element. More than one Get and/or Post is allowed to support including alternative URLs for uses such as load balancing or backup. */ + readonly HTTP: HTTPType; +} + +interface OperationType { + /** Name or identifier of this operation (request) (for example, GetCapabilities). The list of required and optional operations implemented shall be specified in the Implementation Specification for this service. */ + readonly name: string; + + /** Optional unordered list of valid domain constraints on non-parameter quantities that each apply to this operation. If one of these Constraint elements has the same "name" attribute as a Constraint element in the OperationsMetadata element, this Constraint element shall override the other one for this operation. The list of required and optional constraints for this operation shall be specified in the Implementation Specification for this service. */ + readonly Constraint?: DomainType | DomainType[]; + /** Information for one distributed Computing Platform (DCP) supported for this operation. At present, only the HTTP DCP is defined, so this element only includes the HTTP element. */ + readonly DCP: DCPType; + /** Optional unordered list of parameter domains that each apply to this operation which this server implements. If one of these Parameter elements has the same "name" attribute as a Parameter element in the OperationsMetadata element, this Parameter element shall override the other one for this operation. The list of required and optional parameter domain limitations for this operation shall be specified in the Implementation Specification for this service. */ + readonly Parameter?: DomainType | DomainType[]; +} + +interface OperationsMetadataType { + /** Optional unordered list of valid domain constraints on non-parameter quantities that each apply to this server. The list of required and optional constraints shall be specified in the Implementation Specification for this service. */ + readonly Constraint?: DomainType | DomainType[]; + + /** Metadata for one operation that this server implements. */ + readonly Operation: OperationType | OperationType[]; + /** Optional unordered list of parameter valid domains that each apply to one or more operations which this server interface implements. The list of required and optional parameter domain limitations shall be specified in the Implementation Specification for this service. */ + readonly Parameter?: DomainType | DomainType[]; +} + +export interface CapabilitiesBaseType { + /** Service metadata document version, having values that are "increased" whenever any change is made in service metadata document. Values are selected by each server, and are always opaque to clients. When not supported by server, server shall not return this attribute. */ + readonly UpdateSequence?: string; + readonly Version: string; + + /** Metadata about the operations and related abilities specified by this service and implemented by this server, including the URLs for operation requests. The basic contents of this section shall be the same for all OWS types, but individual services can add elements and/or change the optionality of optional elements. */ + readonly OperationsMetadata?: OperationsMetadataType; + /** General metadata for this specific server. This XML Schema of this section shall be the same for all OWS. */ + readonly ServiceIdentification?: ServiceIdentification; + /** Metadata about the organization that provides this specific service instance or server. */ + readonly ServiceProvider?: ServiceProvider; +} + export interface CapabilitiesStyle { readonly Identifier: string; readonly Name: string; diff --git a/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts b/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts index 18cb04ff5aa..2b63f21fc52 100644 --- a/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts +++ b/lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts @@ -9,8 +9,8 @@ import { CapabilitiesLegend, OnlineResource, OwsKeywordList, - ServiceIdentification, - ServiceProvider + type CapabilitiesBaseType, + type RequestMethodType } from "./OwsInterfaces"; export interface WmtsLayer { @@ -59,31 +59,14 @@ export interface CapabilitiesStyle { readonly isDefault?: boolean; } -interface CapabilitiesJson { - readonly Version: string; +interface WMTSCapabilitiesJson extends CapabilitiesBaseType { readonly Contents?: Contents; - readonly ServiceIdentification?: ServiceIdentification; - readonly ServiceProvider?: ServiceProvider; - readonly OperationsMetadata?: OperationsMetadata; readonly ServiceMetadataURL?: OnlineResource; } -interface OperationsMetadata { - readonly Operation: Operation; -} - -interface Operation { - name: string; - DCP: { - HTTP: { - Get?: OnlineResource; - }; - }; -} - interface Contents { - readonly Layer: WmtsLayer; - readonly TileMatrixSet: TileMatrixSet; + readonly Layer: WmtsLayer | WmtsLayer[]; + readonly TileMatrixSet: TileMatrixSet | TileMatrixSet[]; } export interface TileMatrixSetLink { @@ -145,37 +128,18 @@ export default class WebMapTileServiceCapabilities { }); } - return new WebMapTileServiceCapabilities(capabilitiesXml, json); + return new WebMapTileServiceCapabilities(json); }); }); readonly layers: WmtsLayer[]; readonly tileMatrixSets: TileMatrixSet[]; - private constructor( - readonly xml: XMLDocument, - readonly json: CapabilitiesJson - ) { - this.layers = []; - this.tileMatrixSets = []; - - const layerElements = this.json.Contents?.Layer as - | Array - | WmtsLayer; - if (layerElements && Array.isArray(layerElements)) { - this.layers.push(...layerElements); - } else if (layerElements) { - this.layers.push(layerElements as WmtsLayer); - } - - const tileMatrixSetsElements = this.json.Contents?.TileMatrixSet as - | Array - | TileMatrixSet; - if (tileMatrixSetsElements && Array.isArray(tileMatrixSetsElements)) { - this.tileMatrixSets.push(...tileMatrixSetsElements); - } else if (tileMatrixSetsElements) { - this.tileMatrixSets.push(tileMatrixSetsElements as TileMatrixSet); - } + private constructor(private readonly json: WMTSCapabilitiesJson) { + this.layers = this.parseLayers(this.json.Contents?.Layer); + this.tileMatrixSets = this.parseTileMatrixSets( + this.json.Contents?.TileMatrixSet + ); } get ServiceIdentification() { @@ -183,7 +147,32 @@ export default class WebMapTileServiceCapabilities { } get OperationsMetadata() { - return this.json.OperationsMetadata; + const operationsMetadata = this.json.OperationsMetadata; + if (!operationsMetadata) { + return undefined; + } + + const operation = Array.isArray(operationsMetadata.Operation) + ? operationsMetadata.Operation + : [operationsMetadata.Operation]; + return operation.reduce( + (acc, operation) => { + if (!acc[operation.name]) { + acc[operation.name] = { + Get: Array.isArray(operation.DCP.HTTP.Get) + ? operation.DCP.HTTP.Get + : [operation.DCP.HTTP.Get] + }; + } + return acc; + }, + {} as Record< + string, + { + Get: RequestMethodType[]; + } + > + ); } get ServiceProvider() { @@ -232,4 +221,22 @@ export default class WebMapTileServiceCapabilities { (tileMatrixSet) => tileMatrixSet.Identifier === set ); } + + private parseLayers(layers: WmtsLayer | WmtsLayer[] | undefined) { + if (!layers) { + return []; + } + + return Array.isArray(layers) ? layers : [layers]; + } + + private parseTileMatrixSets( + tileMatrixSets: TileMatrixSet | TileMatrixSet[] | undefined + ) { + if (!tileMatrixSets) { + return []; + } + + return Array.isArray(tileMatrixSets) ? tileMatrixSets : [tileMatrixSets]; + } } diff --git a/lib/Models/Catalog/Ows/WebMapTileServiceCatalogItem.ts b/lib/Models/Catalog/Ows/WebMapTileServiceCatalogItem.ts index 8af26fd1ed5..4edeb74dc97 100644 --- a/lib/Models/Catalog/Ows/WebMapTileServiceCatalogItem.ts +++ b/lib/Models/Catalog/Ows/WebMapTileServiceCatalogItem.ts @@ -506,30 +506,11 @@ class WebMapTileServiceCatalogItem extends MappableMixin( format = "image/jpeg"; } - // if layer has defined ResourceURL we should use it because some layers support only Restful encoding. See #2927 - const resourceUrl: ResourceUrl | ResourceUrl[] | undefined = - layer.ResourceURL; - let baseUrl: string = new URI(this.url).search("").toString(); - if (resourceUrl) { - if (Array.isArray(resourceUrl)) { - for (let i = 0; i < resourceUrl.length; i++) { - const url: ResourceUrl = resourceUrl[i]; - if ( - url.format.indexOf(format) !== -1 || - url.format.indexOf("png") !== -1 - ) { - baseUrl = url.template; - } - } - } else { - if ( - format === resourceUrl.format || - resourceUrl.format.indexOf("png") !== -1 - ) { - baseUrl = resourceUrl.template; - } - } - } + const baseUrl: string = this.getTileUrl( + layer, + stratum.capabilities, + format + ); const tileMatrixSet = this.tileMatrixSet; if (!isDefined(tileMatrixSet)) { @@ -556,6 +537,59 @@ class WebMapTileServiceCatalogItem extends MappableMixin( return imageryProvider; } + getTileUrl( + layer: WmtsLayer, + capabilities: WebMapTileServiceCapabilities, + format: string + ) { + let url: string | undefined = undefined; + if ( + capabilities.OperationsMetadata && + "GetTile" in capabilities.OperationsMetadata + ) { + const gets = capabilities.OperationsMetadata.GetTile["Get"]; + + for (let i = 0; i < gets.length; i++) { + let constraints = gets[i].Constraint; + if (constraints) { + constraints = Array.isArray(constraints) + ? constraints + : [constraints]; + const getEncodingConstraint = constraints.find( + (element) => element.name === "GetEncoding" + ); + + const encodings = getEncodingConstraint?.AllowedValues?.Value; + if (encodings?.includes("KVP")) { + url = gets[i]["xlink:href"]; + } + } else if (gets[i]["xlink:href"]) { + url = gets[i]["xlink:href"]; + } + } + } + + const resourceUrls: ResourceUrl[] | undefined = + !layer.ResourceURL || Array.isArray(layer.ResourceURL) + ? layer.ResourceURL + : [layer.ResourceURL]; + + if (resourceUrls && (this.requestEncoding === "RESTful" || !url)) { + for (let i = 0; i < resourceUrls.length; i++) { + const resourceUrl: ResourceUrl = resourceUrls[i]; + if ( + (resourceUrl.resourceType === "tile" && + resourceUrl.format.indexOf(format) !== -1) || + resourceUrl.format.indexOf("png") !== -1 + ) { + url = resourceUrl.template; + } + } + } + + return url ?? new URI(this.url).search("").toString(); + } + @computed get tileMatrixSet(): | { diff --git a/lib/Traits/TraitsClasses/WebMapTileServiceCatalogItemTraits.ts b/lib/Traits/TraitsClasses/WebMapTileServiceCatalogItemTraits.ts index 11fc241e0a8..ef34e2314b0 100644 --- a/lib/Traits/TraitsClasses/WebMapTileServiceCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/WebMapTileServiceCatalogItemTraits.ts @@ -80,7 +80,7 @@ export class WebMapTileServiceAvailableLayerStylesTraits extends ModelTraits { opacity: 1 } }) -export default class WebMapServiceCatalogItemTraits extends mixTraits( +export default class WebMapTileServiceCatalogItemTraits extends mixTraits( LayerOrderingTraits, GetCapabilitiesTraits, ImageryProviderTraits, @@ -124,4 +124,12 @@ export default class WebMapServiceCatalogItemTraits extends mixTraits( "Additional parameters to pass to the MapServer when requesting images." }) parameters?: JsonObject; + + @primitiveTrait({ + type: "string", + name: "Encoding", + description: + "The encoding of the tile images. We will try to load the tile images with this encoding, if not available we will fallback to KVP. Supported values are KVP and Restful" + }) + requestEncoding = "RESTful"; } diff --git a/test/Models/Catalog/Ows/WebMapTileServiceCatalogItemSpec.ts b/test/Models/Catalog/Ows/WebMapTileServiceCatalogItemSpec.ts index 22ad54b9271..bceda3fac70 100644 --- a/test/Models/Catalog/Ows/WebMapTileServiceCatalogItemSpec.ts +++ b/test/Models/Catalog/Ows/WebMapTileServiceCatalogItemSpec.ts @@ -121,6 +121,27 @@ describe("WebMapTileServiceCatalogItem", function () { // } // }); + it("should properly generate tile url request", async function () { + runInAction(() => { + wmts.setTrait( + "definition", + "url", + "test/WMTS/with_operation_metadata.xml" + ); + wmts.setTrait( + "definition", + "layer", + "NWSHELF_ANALYSISFORECAST_PHY_004_013/cmems_mod_nws_phy_anfc_0.027deg-3D_PT1H-m_202309/vo" + ); + }); + + await wmts.loadMapItems(); + + expect(wmts.imageryProvider?.url).toBe( + "http://wmts.marine.copernicus.eu/teroWmts?service=WMTS&version=1.0.0&request=GetTile" + ); + }); + it("calculates correct tileMatrixSet", async function () { runInAction(() => { wmts.setTrait("definition", "url", "test/WMTS/with_tilematrix.xml"); diff --git a/wwwroot/test/WMTS/with_operation_metadata.xml b/wwwroot/test/WMTS/with_operation_metadata.xml new file mode 100644 index 00000000000..f2e2f4ef3d3 --- /dev/null +++ b/wwwroot/test/WMTS/with_operation_metadata.xml @@ -0,0 +1,1312 @@ + +