Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 7296 wmts #7371

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions lib/Models/Catalog/Ows/OwsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
zoran995 marked this conversation as resolved.
Show resolved Hide resolved
}

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[];
zoran995 marked this conversation as resolved.
Show resolved Hide resolved
/** 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;
Expand Down
103 changes: 55 additions & 48 deletions lib/Models/Catalog/Ows/WebMapTileServiceCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
CapabilitiesLegend,
OnlineResource,
OwsKeywordList,
ServiceIdentification,
ServiceProvider
type CapabilitiesBaseType,
type RequestMethodType
} from "./OwsInterfaces";

export interface WmtsLayer {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -145,45 +128,51 @@ 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>
| 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>
| 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() {
return this.json.ServiceIdentification;
}

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() {
Expand Down Expand Up @@ -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];
}
}
82 changes: 58 additions & 24 deletions lib/Models/Catalog/Ows/WebMapTileServiceCatalogItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could for (const resourceUrl of resourceUrls) { be used to iterate over the content so the next line can be omitted, or is there some JavaScript-reason to always use index-based iterations in TypeScript?

Same comment for the other for-loops where the index is only used to get the element.

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():
| {
Expand Down
10 changes: 9 additions & 1 deletion lib/Traits/TraitsClasses/WebMapTileServiceCatalogItemTraits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
}
21 changes: 21 additions & 0 deletions test/Models/Catalog/Ows/WebMapTileServiceCatalogItemSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading
Loading