@camptocamp/ogc-client
Advanced tools
Comparing version 1.1.1-dev.377887e to 1.1.1-dev.3e2d3cc
export { default as WfsEndpoint } from './wfs/endpoint.js'; | ||
export type { WfsVersion, WfsFeatureWithProps, WfsFeatureTypeSummary, WfsFeatureTypeBrief, FeatureGeometryType, FeaturePropertyType, WfsFeatureTypeFull, WfsFeatureTypePropDetails, WfsFeatureTypePropsDetails, WfsFeatureTypeUniqueValue, } from './wfs/model.js'; | ||
export type { WfsVersion, WfsFeatureWithProps, WfsFeatureTypeSummary, WfsFeatureTypeBrief, FeatureGeometryType, FeaturePropertyType, WfsFeatureTypeFull, WfsFeatureTypePropDetails, WfsFeatureTypePropsDetails, WfsFeatureTypeUniqueValue, WfsGetFeatureOptions, } from './wfs/model.js'; | ||
export { default as WmsEndpoint } from './wms/endpoint.js'; | ||
@@ -7,8 +7,9 @@ export type { WmsLayerFull, WmsVersion, WmsLayerSummary, WmsLayerAttribution, } from './wms/model.js'; | ||
export type { WmtsLayerDimensionValue, WmtsLayerResourceLink, WmtsEndpointInfo, WmtsLayer, WmtsMatrixSet, } from './wmts/model.js'; | ||
export type { LayerStyle, BoundingBox, FetchOptions, GenericEndpointInfo, } from './shared/models.js'; | ||
export type { Address, Contact, Provider, LayerStyle, BoundingBox, MetadataURL, FetchOptions, GenericEndpointInfo, MimeType, CrsCode, } from './shared/models.js'; | ||
export { default as OgcApiEndpoint } from './ogc-api/endpoint.js'; | ||
export * from './ogc-api/model.js'; | ||
export { useCache } from './shared/cache.js'; | ||
export { useCache, clearCache } from './shared/cache.js'; | ||
export { sharedFetch, setFetchOptions, resetFetchOptions, } from './shared/http-utils.js'; | ||
export { check, default as ServiceExceptionError, } from './shared/service-exception-error.js'; | ||
export { enableFallbackWithoutWorker } from './worker/index.js'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -6,3 +6,3 @@ import { default as default2 } from "./wfs/endpoint.js"; | ||
export * from "./ogc-api/model.js"; | ||
import { useCache } from "./shared/cache.js"; | ||
import { useCache, clearCache } from "./shared/cache.js"; | ||
import { | ||
@@ -13,2 +13,6 @@ sharedFetch, | ||
} from "./shared/http-utils.js"; | ||
import { | ||
check, | ||
default as default6 | ||
} from "./shared/service-exception-error.js"; | ||
import { enableFallbackWithoutWorker } from "./worker/index.js"; | ||
@@ -18,5 +22,8 @@ import "./worker-fallback/index.js"; | ||
default5 as OgcApiEndpoint, | ||
default6 as ServiceExceptionError, | ||
default2 as WfsEndpoint, | ||
default3 as WmsEndpoint, | ||
default4 as WmtsEndpoint, | ||
check, | ||
clearCache, | ||
enableFallbackWithoutWorker, | ||
@@ -23,0 +30,0 @@ resetFetchOptions, |
@@ -31,3 +31,7 @@ /** | ||
*/ | ||
export declare function purgeEntries(): Promise<void>; | ||
export declare function purgeOutdatedEntries(): Promise<void>; | ||
/** | ||
* Remove all cache entries; will not prevent the creation of new ones | ||
*/ | ||
export declare function clearCache(): Promise<void>; | ||
//# sourceMappingURL=cache.d.ts.map |
@@ -67,3 +67,3 @@ let cacheExpiryDuration = 1e3 * 60 * 60; | ||
async function useCache(factory, ...keys) { | ||
await purgeEntries(); | ||
await purgeOutdatedEntries(); | ||
if (await hasValidCacheEntry(...keys)) { | ||
@@ -85,3 +85,3 @@ return readCacheEntry(...keys); | ||
} | ||
async function purgeEntries() { | ||
async function purgeOutdatedEntries() { | ||
const cache = await getCache(); | ||
@@ -97,8 +97,18 @@ if (!cache) | ||
} | ||
async function clearCache() { | ||
const cache = await getCache(); | ||
if (!cache) | ||
return; | ||
const keys = await cache.keys(); | ||
for (const key of keys) { | ||
await cache.delete(key); | ||
} | ||
} | ||
export { | ||
_resetCache, | ||
clearCache, | ||
getCache, | ||
getCacheExpiryDuration, | ||
hasValidCacheEntry, | ||
purgeEntries, | ||
purgeOutdatedEntries, | ||
readCacheEntry, | ||
@@ -105,0 +115,0 @@ setCacheExpiryDuration, |
@@ -6,2 +6,23 @@ /** | ||
export type CrsCode = string; | ||
export interface Address { | ||
deliveryPoint?: string; | ||
city?: string; | ||
administrativeArea?: string; | ||
postalCode?: string; | ||
country?: string; | ||
} | ||
export interface Contact { | ||
name?: string; | ||
organization?: string; | ||
position?: string; | ||
phone?: string; | ||
fax?: string; | ||
address?: Address; | ||
email?: string; | ||
} | ||
export interface Provider { | ||
name?: string; | ||
site?: string; | ||
contact?: Contact; | ||
} | ||
export type GenericEndpointInfo = { | ||
@@ -14,3 +35,18 @@ name: string; | ||
keywords: string[]; | ||
provider?: Provider; | ||
/** | ||
* Can contain the list of outputFormats from a WFS GetCapabilities, | ||
* or the list of 'Formats' from a WMS GetCapabilities | ||
*/ | ||
outputFormats?: MimeType[]; | ||
/** | ||
* Contains a list of formats that can be used for WMS GetFeatureInfo, | ||
* or undefined for other services such as WFS | ||
*/ | ||
infoFormats?: MimeType[]; | ||
/** | ||
* Contains a list of formats that can be used for Exceptions for WMS GetMap, | ||
* or undefined for other services such as WFS | ||
*/ | ||
exceptionFormats?: MimeType[] | string[]; | ||
}; | ||
@@ -26,2 +62,5 @@ export type MimeType = string; | ||
} | ||
export type OperationName = string; | ||
export type HttpMethod = 'Get' | 'Post'; | ||
export type OperationUrl = Partial<Record<HttpMethod, string>>; | ||
export interface LayerStyle { | ||
@@ -36,2 +75,7 @@ name: string; | ||
} | ||
export type MetadataURL = { | ||
format?: string; | ||
type?: string; | ||
url: string; | ||
}; | ||
//# sourceMappingURL=models.d.ts.map |
import { XmlDocument } from '@rgrove/parse-xml'; | ||
import { GenericEndpointInfo, MimeType } from '../shared/models.js'; | ||
import { GenericEndpointInfo, MimeType, type OperationName, type OperationUrl } from '../shared/models.js'; | ||
import { WfsFeatureTypeInternal, WfsVersion } from './model.js'; | ||
/** | ||
* Will read the operation URLS from the capabilities doc | ||
* @param capabilitiesDoc Capabilities document | ||
*/ | ||
export declare function readOperationUrlsFromCapabilities(capabilitiesDoc: XmlDocument): Record<OperationName, OperationUrl>; | ||
/** | ||
* Will read a WFS version from the capabilities doc | ||
@@ -6,0 +11,0 @@ * @param capabilitiesDoc Capabilities document |
@@ -0,1 +1,2 @@ | ||
import { readProviderFromCapabilities } from "../shared/ows.js"; | ||
import { | ||
@@ -8,5 +9,30 @@ findChildElement, | ||
getElementText, | ||
getRootElement | ||
getRootElement, | ||
stripNamespace | ||
} from "../shared/xml-utils.js"; | ||
import { simplifyEpsgUrn } from "../shared/crs-utils.js"; | ||
function readOperationUrlsFromCapabilities(capabilitiesDoc) { | ||
const urls = {}; | ||
const capabilities = getRootElement(capabilitiesDoc); | ||
const operationsMetadata = findChildElement( | ||
capabilities, | ||
"OperationsMetadata" | ||
); | ||
if (operationsMetadata) { | ||
findChildrenElement(operationsMetadata, "Operation").forEach( | ||
(operation) => { | ||
const name = getElementAttribute(operation, "name"); | ||
urls[name] = parseOperation110(operation); | ||
} | ||
); | ||
} else { | ||
const capability = findChildElement(capabilities, "Capability"); | ||
const request = findChildElement(capability, "Request"); | ||
getChildrenElement(request).forEach((operation) => { | ||
const name = stripNamespace(getElementName(operation)); | ||
urls[name] = parseOperation100(operation); | ||
}); | ||
} | ||
return urls; | ||
} | ||
function readVersionFromCapabilities(capabilitiesDoc) { | ||
@@ -60,2 +86,6 @@ return getRootElement(capabilitiesDoc).attributes["version"]; | ||
} | ||
let provider; | ||
if (version !== "1.0.0") { | ||
provider = readProviderFromCapabilities(capabilitiesDoc); | ||
} | ||
return { | ||
@@ -68,2 +98,3 @@ title: getElementText(findChildElement(service, "Title")), | ||
keywords, | ||
provider, | ||
outputFormats: readOutputFormatsFromCapabilities(capabilitiesDoc) | ||
@@ -83,2 +114,24 @@ }; | ||
} | ||
function parseOperation100(operation) { | ||
const urls = {}; | ||
const dcpType = findChildrenElement(operation, "DCPType"); | ||
const http = dcpType.flatMap((d) => findChildrenElement(d, "HTTP")); | ||
const methods = http.flatMap((h) => getChildrenElement(h)); | ||
methods.forEach((method) => { | ||
const methodName = stripNamespace(getElementName(method)); | ||
urls[methodName] = getElementAttribute(method, "onlineResource"); | ||
}); | ||
return urls; | ||
} | ||
function parseOperation110(operation) { | ||
const urls = {}; | ||
const dcpType = findChildrenElement(operation, "DCP"); | ||
const http = dcpType.flatMap((d) => findChildElement(d, "HTTP")); | ||
const methods = http.flatMap((h) => getChildrenElement(h)); | ||
methods.forEach((method) => { | ||
const methodName = stripNamespace(getElementName(method)); | ||
urls[methodName] = getElementAttribute(method, "xlink:href"); | ||
}); | ||
return urls; | ||
} | ||
function parseFeatureType(featureTypeEl, serviceVersion, defaultOutputFormats) { | ||
@@ -104,2 +157,13 @@ const srsTag = serviceVersion.startsWith("2.") ? "CRS" : "SRS"; | ||
).map(getElementText).filter((v, i, arr) => arr.indexOf(v) === i); | ||
const metadata = serviceVersion === "2.0.0" ? findChildrenElement(featureTypeEl, "MetadataURL").map( | ||
(metadataUrlEl) => ({ | ||
url: getElementAttribute(metadataUrlEl, "xlink:href") | ||
}) | ||
) : findChildrenElement(featureTypeEl, "MetadataURL").map( | ||
(metadataUrlEl) => ({ | ||
format: getElementAttribute(metadataUrlEl, "format"), | ||
type: getElementAttribute(metadataUrlEl, "type"), | ||
url: getElementText(metadataUrlEl).trim() | ||
}) | ||
); | ||
return { | ||
@@ -115,3 +179,4 @@ name: getElementText(findChildElement(featureTypeEl, "Name")), | ||
latLonBoundingBox: serviceVersion.startsWith("1.0") ? parseBBox100() : parseBBox(), | ||
keywords | ||
keywords, | ||
...metadata.length && { metadata } | ||
}; | ||
@@ -122,2 +187,3 @@ } | ||
readInfoFromCapabilities, | ||
readOperationUrlsFromCapabilities, | ||
readOutputFormatsFromCapabilities, | ||
@@ -124,0 +190,0 @@ readVersionFromCapabilities |
@@ -1,3 +0,3 @@ | ||
import { BoundingBox, CrsCode, GenericEndpointInfo, MimeType } from '../shared/models.js'; | ||
import { WfsFeatureTypeBrief, WfsFeatureTypeSummary, WfsVersion } from './model.js'; | ||
import { GenericEndpointInfo, type HttpMethod, type OperationName } from '../shared/models.js'; | ||
import { WfsFeatureTypeBrief, WfsFeatureTypeSummary, WfsGetFeatureOptions, WfsVersion } from './model.js'; | ||
/** | ||
@@ -11,2 +11,3 @@ * Represents a WFS endpoint advertising several feature types | ||
private _featureTypes; | ||
private _url; | ||
private _version; | ||
@@ -76,22 +77,20 @@ /** | ||
* @param featureType | ||
* @param {Object} [options] | ||
* @property [options.maxFeatures] no limit if undefined | ||
* @property [options.asJson] if true, will ask for GeoJSON; will throw if the service does not support it | ||
* @property [options.outputFormat] a supported output format (overridden by `asJson`) | ||
* @property [options.outputCrs] if unspecified, this will be the data native projection | ||
* @property [options.extent] an extent to restrict returned objects | ||
* @property [options.extentCrs] if unspecified, `extent` should be in the data native projection | ||
* @property [options.startIndex] if the service supports it, this will be the index of the first feature to return | ||
* @param options | ||
* @returns Returns null if endpoint is not ready | ||
*/ | ||
getFeatureUrl(featureType: string, options: { | ||
maxFeatures?: number; | ||
asJson?: boolean; | ||
outputFormat?: MimeType; | ||
outputCrs?: CrsCode; | ||
extent?: BoundingBox; | ||
extentCrs?: CrsCode; | ||
startIndex?: number; | ||
}): string; | ||
getFeatureUrl(featureType: string, options?: WfsGetFeatureOptions): string; | ||
/** | ||
* Returns the Capabilities URL of the WMS | ||
* | ||
* This is the URL reported by the service if available, otherwise the URL | ||
* passed to the constructor | ||
*/ | ||
getCapabilitiesUrl(): string; | ||
/** | ||
* Returns the URL reported by the WFS for the given operation | ||
* @param operationName e.g. GetFeature, GetCapabilities, etc. | ||
* @param method HTTP method | ||
*/ | ||
getOperationUrl(operationName: OperationName, method?: HttpMethod): string; | ||
} | ||
//# sourceMappingURL=endpoint.d.ts.map |
@@ -19,2 +19,3 @@ import { | ||
_featureTypes; | ||
_url; | ||
_version; | ||
@@ -36,5 +37,6 @@ /** | ||
this._capabilitiesUrl | ||
).then(({ info, featureTypes, version }) => { | ||
).then(({ info, featureTypes, url: url2, version }) => { | ||
this._info = info; | ||
this._featureTypes = featureTypes; | ||
this._url = url2; | ||
this._version = version; | ||
@@ -99,3 +101,4 @@ }); | ||
outputFormats: featureType.outputFormats, | ||
keywords: featureType.keywords | ||
keywords: featureType.keywords, | ||
..."metadata" in featureType && { metadata: featureType.metadata } | ||
}; | ||
@@ -116,3 +119,3 @@ } | ||
const describeUrl = generateDescribeFeatureTypeUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl("DescribeFeatureType"), | ||
this._version, | ||
@@ -122,3 +125,3 @@ name | ||
const getFeatureUrl = generateGetFeatureUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl("GetFeature"), | ||
this._version, | ||
@@ -220,10 +223,3 @@ name, | ||
* @param featureType | ||
* @param {Object} [options] | ||
* @property [options.maxFeatures] no limit if undefined | ||
* @property [options.asJson] if true, will ask for GeoJSON; will throw if the service does not support it | ||
* @property [options.outputFormat] a supported output format (overridden by `asJson`) | ||
* @property [options.outputCrs] if unspecified, this will be the data native projection | ||
* @property [options.extent] an extent to restrict returned objects | ||
* @property [options.extentCrs] if unspecified, `extent` should be in the data native projection | ||
* @property [options.startIndex] if the service supports it, this will be the index of the first feature to return | ||
* @param options | ||
* @returns Returns null if endpoint is not ready | ||
@@ -242,3 +238,5 @@ */ | ||
extentCrs, | ||
startIndex | ||
startIndex, | ||
attributes, | ||
hitsOnly | ||
} = options || {}; | ||
@@ -265,3 +263,3 @@ const internalFeatureType = this._getFeatureTypeByName(featureType); | ||
return generateGetFeatureUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl("GetFeature"), | ||
this._version, | ||
@@ -271,4 +269,4 @@ internalFeatureType.name, | ||
maxFeatures, | ||
void 0, | ||
void 0, | ||
attributes, | ||
hitsOnly, | ||
outputCrs, | ||
@@ -280,2 +278,29 @@ extent, | ||
} | ||
/** | ||
* Returns the Capabilities URL of the WMS | ||
* | ||
* This is the URL reported by the service if available, otherwise the URL | ||
* passed to the constructor | ||
*/ | ||
getCapabilitiesUrl() { | ||
const baseUrl = this.getOperationUrl("GetCapabilities"); | ||
if (!baseUrl) { | ||
return this._capabilitiesUrl; | ||
} | ||
return setQueryParams(baseUrl, { | ||
SERVICE: "WMS", | ||
REQUEST: "GetCapabilities" | ||
}); | ||
} | ||
/** | ||
* Returns the URL reported by the WFS for the given operation | ||
* @param operationName e.g. GetFeature, GetCapabilities, etc. | ||
* @param method HTTP method | ||
*/ | ||
getOperationUrl(operationName, method = "Get") { | ||
if (!this._url) { | ||
return null; | ||
} | ||
return this._url[operationName]?.[method]; | ||
} | ||
} | ||
@@ -282,0 +307,0 @@ export { |
@@ -15,3 +15,4 @@ import { | ||
latLonBoundingBox: boundingBox, | ||
keywords | ||
keywords, | ||
metadata | ||
} = featureType; | ||
@@ -28,3 +29,3 @@ const hitsAttr = serviceVersion.startsWith("2.0") ? "numberMatched" : "numberOfFeatures"; | ||
const typeElementsEls = findChildrenElement(complexTypeEl, "element", true); | ||
const properties = typeElementsEls.filter((el) => getElementAttribute(el, "type").startsWith("xsd:")).reduce( | ||
const properties = typeElementsEls.filter((el) => /^xsd:|^xs:/.test(getElementAttribute(el, "type"))).reduce( | ||
(prev, curr) => ({ | ||
@@ -55,3 +56,4 @@ ...prev, | ||
...!Number.isNaN(objectCount) && { objectCount }, | ||
...keywords && { keywords } | ||
...keywords && { keywords }, | ||
...metadata && { metadata } | ||
}; | ||
@@ -58,0 +60,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { BoundingBox, CrsCode, MimeType } from '../shared/models.js'; | ||
import { BoundingBox, CrsCode, MetadataURL, MimeType } from '../shared/models.js'; | ||
export type WfsVersion = '1.0.0' | '1.1.0' | '2.0.0'; | ||
@@ -12,2 +12,3 @@ export type WfsFeatureTypeInternal = { | ||
keywords?: string[]; | ||
metadata?: MetadataURL[]; | ||
}; | ||
@@ -37,2 +38,3 @@ export type FeaturePropertyType = string | number | boolean; | ||
keywords?: string[]; | ||
metadata?: MetadataURL[]; | ||
}; | ||
@@ -86,2 +88,42 @@ export type WfsFeatureTypeFull = { | ||
export type WfsFeatureTypePropsDetails = Record<string, WfsFeatureTypePropDetails>; | ||
export type WfsGetFeatureOptions = { | ||
/** | ||
* No limit if undefined | ||
*/ | ||
maxFeatures?: number; | ||
/** | ||
* if true, will ask for GeoJSON; will throw if the service does not support it | ||
*/ | ||
asJson?: boolean; | ||
/** | ||
* a supported output format (overridden by `asJson`) | ||
*/ | ||
outputFormat?: MimeType; | ||
/** | ||
* if unspecified, this will be the data native projection | ||
*/ | ||
outputCrs?: CrsCode; | ||
/** | ||
* an extent to restrict returned objects | ||
*/ | ||
extent?: BoundingBox; | ||
/** | ||
* if unspecified, `extent` should be in the data native projection | ||
*/ | ||
extentCrs?: CrsCode; | ||
/** | ||
* if the service supports it, this will be the index of the first feature to return | ||
*/ | ||
startIndex?: number; | ||
/** | ||
* if not defined, all attributes will be included; note that the fact | ||
* that these attributes exist or not will not be checked! | ||
*/ | ||
attributes?: string[]; | ||
/** | ||
* if true, will not return feature data, only hit count | ||
* note: this might not work for WFS version < 2 | ||
*/ | ||
hitsOnly?: boolean; | ||
}; | ||
//# sourceMappingURL=model.d.ts.map |
@@ -5,2 +5,8 @@ import { XmlDocument } from '@rgrove/parse-xml'; | ||
/** | ||
* Will read all operation URLs from the capabilities doc | ||
* @param capabilitiesDoc Capabilities document | ||
* @return The parsed operations URLs | ||
*/ | ||
export declare function readOperationUrlsFromCapabilities(capabilitiesDoc: XmlDocument): Record<string, Partial<Record<import("../shared/models.js").HttpMethod, string>>>; | ||
/** | ||
* Will read a WMS version from the capabilities doc | ||
@@ -17,3 +23,11 @@ * @param capabilitiesDoc Capabilities document | ||
export declare function readLayersFromCapabilities(capabilitiesDoc: XmlDocument): WmsLayerFull[]; | ||
export declare function readOutputFormatsFromCapabilities(capabilitiesDoc: XmlDocument): string[]; | ||
export declare function readInfoFormatsFromCapabilities(capabilitiesDoc: XmlDocument): string[]; | ||
/** | ||
* Will return all available exception formats | ||
* @param capabilitiesDoc Capabiliites document | ||
* @return Available exception formats | ||
*/ | ||
export declare function readExceptionFormatsFromCapabilities(capabilitiesDoc: XmlDocument): string[]; | ||
/** | ||
* Will read service-related info from the capabilities doc | ||
@@ -20,0 +34,0 @@ * @param capabilitiesDoc Capabilities document |
@@ -0,9 +1,25 @@ | ||
import { hasInvertedCoordinates } from "../shared/crs-utils.js"; | ||
import { | ||
findChildElement, | ||
findChildrenElement, | ||
getChildrenElement, | ||
getElementAttribute, | ||
getElementName, | ||
getElementText, | ||
getRootElement | ||
getRootElement, | ||
stripNamespace | ||
} from "../shared/xml-utils.js"; | ||
import { hasInvertedCoordinates } from "../shared/crs-utils.js"; | ||
function readOperationUrlsFromCapabilities(capabilitiesDoc) { | ||
const urls = {}; | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
"Capability" | ||
); | ||
const request = findChildElement(capability, "Request"); | ||
getChildrenElement(request).forEach((operation) => { | ||
const operationName = stripNamespace(getElementName(operation)); | ||
urls[operationName] = parseOperation(operation); | ||
}); | ||
return urls; | ||
} | ||
function readVersionFromCapabilities(capabilitiesDoc) { | ||
@@ -22,4 +38,46 @@ return getRootElement(capabilitiesDoc).attributes["version"]; | ||
} | ||
function readOutputFormatsFromCapabilities(capabilitiesDoc) { | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
"Capability" | ||
); | ||
const getMap = findChildElement( | ||
findChildElement(capability, "Request"), | ||
"GetMap" | ||
); | ||
const outputFormats = findChildrenElement(getMap, "Format").map( | ||
getElementText | ||
); | ||
return outputFormats; | ||
} | ||
function readInfoFormatsFromCapabilities(capabilitiesDoc) { | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
"Capability" | ||
); | ||
const getFeatureInfo = findChildElement( | ||
findChildElement(capability, "Request"), | ||
"GetFeatureInfo" | ||
); | ||
const outputFormats = findChildrenElement(getFeatureInfo, "Format").map( | ||
getElementText | ||
); | ||
return outputFormats; | ||
} | ||
function readExceptionFormatsFromCapabilities(capabilitiesDoc) { | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
"Capability" | ||
); | ||
const exception = findChildElement(capability, "Exception"); | ||
const exceptionFormats = findChildrenElement(exception, "Format").map( | ||
getElementText | ||
); | ||
return exceptionFormats; | ||
} | ||
function readInfoFromCapabilities(capabilitiesDoc) { | ||
const service = findChildElement(getRootElement(capabilitiesDoc), "Service"); | ||
const outputFormats = readOutputFormatsFromCapabilities(capabilitiesDoc); | ||
const infoFormats = readInfoFormatsFromCapabilities(capabilitiesDoc); | ||
const exceptionFormats = readExceptionFormatsFromCapabilities(capabilitiesDoc); | ||
const keywords = findChildrenElement( | ||
@@ -29,2 +87,3 @@ findChildElement(service, "KeywordList"), | ||
).map(getElementText).filter((v, i, arr) => arr.indexOf(v) === i); | ||
const provider = readProviderFromCapabilities(capabilitiesDoc); | ||
return { | ||
@@ -34,8 +93,24 @@ title: getElementText(findChildElement(service, "Title")), | ||
abstract: getElementText(findChildElement(service, "Abstract")), | ||
outputFormats, | ||
infoFormats, | ||
exceptionFormats, | ||
fees: getElementText(findChildElement(service, "Fees")), | ||
constraints: getElementText(findChildElement(service, "AccessConstraints")), | ||
provider, | ||
keywords | ||
}; | ||
} | ||
function parseLayer(layerEl, version, inheritedSrs = [], inheritedStyles = [], inheritedAttribution = null, inheritedBoundingBoxes = null) { | ||
function parseOperation(operation) { | ||
const urls = {}; | ||
const dcpType = findChildrenElement(operation, "DCPType"); | ||
const http = dcpType.flatMap((d) => findChildElement(d, "HTTP")); | ||
const methods = http.flatMap((h) => getChildrenElement(h)); | ||
methods.forEach((method) => { | ||
const onlineResource = findChildElement(method, "OnlineResource"); | ||
const methodName = stripNamespace(getElementName(method)); | ||
urls[methodName] = getElementAttribute(onlineResource, "xlink:href"); | ||
}); | ||
return urls; | ||
} | ||
function parseLayer(layerEl, version, inheritedSrs = [], inheritedStyles = [], inheritedAttribution = null, inheritedBoundingBoxes = null, inheritedMaxScaleDenom = null, inheritedMinScaleDenom = null) { | ||
const srsTag = version === "1.3.0" ? "CRS" : "SRS"; | ||
@@ -66,2 +141,24 @@ const srsList = findChildrenElement(layerEl, srsTag).map(getElementText); | ||
} | ||
function parseScaleHintValue(textValue, defaultValue) { | ||
if (textValue === "") { | ||
return defaultValue; | ||
} | ||
return Math.sqrt(0.5 * parseFloat(textValue) ** 2) / 28e-5; | ||
} | ||
function parseScaleHint() { | ||
const scaleHint = findChildElement(layerEl, "ScaleHint"); | ||
if (!scaleHint) { | ||
return [inheritedMinScaleDenom, inheritedMaxScaleDenom]; | ||
} | ||
const min = getElementAttribute(scaleHint, "min"); | ||
const max = getElementAttribute(scaleHint, "max"); | ||
return [ | ||
parseScaleHintValue(min, inheritedMinScaleDenom), | ||
parseScaleHintValue(max, inheritedMaxScaleDenom) | ||
]; | ||
} | ||
function parseScaleDenominator(name, inheritedValue) { | ||
const textValue = getElementText(findChildElement(layerEl, name)); | ||
return textValue === "" ? inheritedValue : parseFloat(textValue); | ||
} | ||
const attributionEl = findChildElement(layerEl, "Attribution"); | ||
@@ -88,4 +185,36 @@ const attribution = attributionEl !== null ? parseLayerAttribution(attributionEl) : inheritedAttribution; | ||
).map(getElementText).filter((v, i, arr) => arr.indexOf(v) === i); | ||
let minScaleDenominator, maxScaleDenominator; | ||
if (version === "1.3.0") { | ||
minScaleDenominator = parseScaleDenominator( | ||
"MinScaleDenominator", | ||
inheritedMinScaleDenom | ||
); | ||
maxScaleDenominator = parseScaleDenominator( | ||
"MaxScaleDenominator", | ||
inheritedMaxScaleDenom | ||
); | ||
} else { | ||
[minScaleDenominator, maxScaleDenominator] = parseScaleHint(); | ||
} | ||
const metadata = findChildrenElement(layerEl, "MetadataURL").map( | ||
(metadataUrlEl) => ({ | ||
type: getElementAttribute(metadataUrlEl, "type"), | ||
format: getElementText(findChildElement(metadataUrlEl, "Format")), | ||
url: getElementAttribute( | ||
findChildElement(metadataUrlEl, "OnlineResource"), | ||
"xlink:href" | ||
) | ||
}) | ||
); | ||
const children = findChildrenElement(layerEl, "Layer").map( | ||
(layer) => parseLayer(layer, version, availableCrs, styles, attribution, boundingBoxes) | ||
(layer) => parseLayer( | ||
layer, | ||
version, | ||
availableCrs, | ||
styles, | ||
attribution, | ||
boundingBoxes, | ||
maxScaleDenominator, | ||
minScaleDenominator | ||
) | ||
); | ||
@@ -103,2 +232,5 @@ return { | ||
opaque, | ||
...minScaleDenominator !== null ? { minScaleDenominator } : {}, | ||
...maxScaleDenominator !== null ? { maxScaleDenominator } : {}, | ||
...metadata.length && { metadata }, | ||
...children.length && { children } | ||
@@ -139,7 +271,51 @@ }; | ||
} | ||
function readProviderFromCapabilities(capabilitiesDoc) { | ||
const service = findChildElement(getRootElement(capabilitiesDoc), "Service"); | ||
const contactInformation = findChildElement(service, "ContactInformation"); | ||
const contactPersonPrimary = findChildElement( | ||
contactInformation, | ||
"ContactPersonPrimary" | ||
); | ||
const address = findChildElement(contactInformation, "ContactAddress"); | ||
return { | ||
contact: { | ||
name: getElementText( | ||
findChildElement(contactPersonPrimary, "ContactPerson") | ||
), | ||
organization: getElementText( | ||
findChildElement(contactPersonPrimary, "ContactOrganization") | ||
), | ||
position: getElementText( | ||
findChildElement(contactInformation, "ContactPosition") | ||
), | ||
phone: getElementText( | ||
findChildElement(contactInformation, "ContactVoiceTelephone") | ||
), | ||
fax: getElementText( | ||
findChildElement(contactInformation, "ContactFacsimileTelephone") | ||
), | ||
address: { | ||
deliveryPoint: getElementText(findChildElement(address, "Address")), | ||
city: getElementText(findChildElement(address, "City")), | ||
administrativeArea: getElementText( | ||
findChildElement(address, "StateOrProvince") | ||
), | ||
postalCode: getElementText(findChildElement(address, "PostCode")), | ||
country: getElementText(findChildElement(address, "Country")) | ||
}, | ||
email: getElementText( | ||
findChildElement(contactInformation, "ContactElectronicMailAddress") | ||
) | ||
} | ||
}; | ||
} | ||
export { | ||
readExceptionFormatsFromCapabilities, | ||
readInfoFormatsFromCapabilities, | ||
readInfoFromCapabilities, | ||
readLayersFromCapabilities, | ||
readOperationUrlsFromCapabilities, | ||
readOutputFormatsFromCapabilities, | ||
readVersionFromCapabilities | ||
}; | ||
//# sourceMappingURL=capabilities.js.map |
@@ -1,2 +0,2 @@ | ||
import { BoundingBox, CrsCode, GenericEndpointInfo, MimeType } from '../shared/models.js'; | ||
import { BoundingBox, CrsCode, GenericEndpointInfo, type HttpMethod, MimeType, type OperationName } from '../shared/models.js'; | ||
import { WmsLayerFull, WmsLayerSummary, WmsVersion } from './model.js'; | ||
@@ -11,2 +11,3 @@ /** | ||
private _layers; | ||
private _url; | ||
private _version; | ||
@@ -69,3 +70,16 @@ /** | ||
}): string; | ||
/** | ||
* Returns the Capabilities URL of the WMS | ||
* | ||
* This is the URL reported by the service if available, otherwise the URL | ||
* passed to the constructor | ||
*/ | ||
getCapabilitiesUrl(): string; | ||
/** | ||
* Returns the URL reported by the WMS for the given operation | ||
* @param operationName e.g. GetMap, GetCapabilities, etc. | ||
* @param method HTTP method | ||
*/ | ||
getOperationUrl(operationName: OperationName, method?: HttpMethod): string; | ||
} | ||
//# sourceMappingURL=endpoint.d.ts.map |
@@ -10,2 +10,3 @@ import { parseWmsCapabilities } from "../worker/index.js"; | ||
_layers; | ||
_url; | ||
_version; | ||
@@ -26,5 +27,6 @@ /** | ||
this._capabilitiesUrl | ||
).then(({ info, layers, version }) => { | ||
).then(({ info, layers, url: url2, version }) => { | ||
this._info = info; | ||
this._layers = layers; | ||
this._url = url2; | ||
this._version = version; | ||
@@ -131,3 +133,3 @@ }); | ||
return generateGetMapUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl("GetMap") || this._capabilitiesUrl, | ||
this._version, | ||
@@ -143,2 +145,29 @@ layers.join(","), | ||
} | ||
/** | ||
* Returns the Capabilities URL of the WMS | ||
* | ||
* This is the URL reported by the service if available, otherwise the URL | ||
* passed to the constructor | ||
*/ | ||
getCapabilitiesUrl() { | ||
const baseUrl = this.getOperationUrl("GetCapabilities"); | ||
if (!baseUrl) { | ||
return this._capabilitiesUrl; | ||
} | ||
return setQueryParams(baseUrl, { | ||
SERVICE: "WMS", | ||
REQUEST: "GetCapabilities" | ||
}); | ||
} | ||
/** | ||
* Returns the URL reported by the WMS for the given operation | ||
* @param operationName e.g. GetMap, GetCapabilities, etc. | ||
* @param method HTTP method | ||
*/ | ||
getOperationUrl(operationName, method = "Get") { | ||
if (!this._url) { | ||
return null; | ||
} | ||
return this._url[operationName]?.[method]; | ||
} | ||
} | ||
@@ -145,0 +174,0 @@ export { |
@@ -1,2 +0,2 @@ | ||
import { BoundingBox, CrsCode, LayerStyle } from '../shared/models.js'; | ||
import { BoundingBox, CrsCode, LayerStyle, type MetadataURL } from '../shared/models.js'; | ||
export type WmsLayerAttribution = { | ||
@@ -34,4 +34,7 @@ title?: string; | ||
opaque: boolean; | ||
maxScaleDenominator?: number; | ||
minScaleDenominator?: number; | ||
attribution?: WmsLayerAttribution; | ||
keywords?: string[]; | ||
metadata?: MetadataURL[]; | ||
/** | ||
@@ -38,0 +41,0 @@ * Not defined if the layer is a leaf in the tree |
@@ -0,1 +1,2 @@ | ||
import { readProviderFromCapabilities } from "../shared/ows.js"; | ||
import { | ||
@@ -44,2 +45,3 @@ findChildElement, | ||
keywords, | ||
provider: readProviderFromCapabilities(capabilitiesDoc), | ||
getTileUrls | ||
@@ -46,0 +48,0 @@ }; |
@@ -1,2 +0,2 @@ | ||
import { GenericEndpointInfo } from '../shared/models.js'; | ||
import { GenericEndpointInfo, OperationName, OperationUrl } from '../shared/models.js'; | ||
import { WmtsEndpointInfo, WmtsLayer, WmtsMatrixSet } from '../wmts/model.js'; | ||
@@ -15,2 +15,3 @@ import { WfsFeatureTypeFull, WfsFeatureTypeInternal, WfsFeatureTypePropsDetails, WfsVersion } from '../wfs/model.js'; | ||
version: WmsVersion; | ||
url: Record<OperationName, OperationUrl>; | ||
info: GenericEndpointInfo; | ||
@@ -25,2 +26,3 @@ layers: WmsLayerFull[]; | ||
version: WfsVersion; | ||
url: Record<OperationName, OperationUrl>; | ||
info: GenericEndpointInfo; | ||
@@ -27,0 +29,0 @@ featureTypes: WfsFeatureTypeInternal[]; |
import { addTaskHandler } from "./utils.js"; | ||
import { queryXmlDocument, setFetchOptions } from "../shared/http-utils.js"; | ||
import { check } from "../shared/service-exception-error.js"; | ||
import * as wmsCapabilities from "../wms/capabilities.js"; | ||
@@ -14,5 +15,6 @@ import * as wfsCapabilities from "../wfs/capabilities.js"; | ||
globalThis, | ||
({ url }) => queryXmlDocument(url).then((xmlDoc) => ({ | ||
({ url }) => queryXmlDocument(url).then((xmlDoc) => check(xmlDoc, url)).then((xmlDoc) => ({ | ||
info: wmsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
layers: wmsCapabilities.readLayersFromCapabilities(xmlDoc), | ||
url: wmsCapabilities.readOperationUrlsFromCapabilities(xmlDoc), | ||
version: wmsCapabilities.readVersionFromCapabilities(xmlDoc) | ||
@@ -24,5 +26,6 @@ })) | ||
globalThis, | ||
({ url }) => queryXmlDocument(url).then((xmlDoc) => ({ | ||
({ url }) => queryXmlDocument(url).then((xmlDoc) => check(xmlDoc, url)).then((xmlDoc) => ({ | ||
info: wfsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
featureTypes: wfsCapabilities.readFeatureTypesFromCapabilities(xmlDoc), | ||
url: wfsCapabilities.readOperationUrlsFromCapabilities(xmlDoc), | ||
version: wfsCapabilities.readVersionFromCapabilities(xmlDoc) | ||
@@ -65,3 +68,3 @@ })) | ||
globalThis, | ||
({ url }) => queryXmlDocument(url).then((xmlDoc) => ({ | ||
({ url }) => queryXmlDocument(url).then((xmlDoc) => check(xmlDoc, url)).then((xmlDoc) => ({ | ||
info: wmtsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
@@ -68,0 +71,0 @@ layers: wmtsCapabilities.readLayersFromCapabilities(xmlDoc), |
{ | ||
"name": "@camptocamp/ogc-client", | ||
"version": "1.1.1-dev.377887e", | ||
"version": "1.1.1-dev.3e2d3cc", | ||
"description": "A pure JS library for interacting with geospatial services.", | ||
@@ -5,0 +5,0 @@ "main": "./dist/dist-node.js", |
@@ -13,2 +13,3 @@ export { default as WfsEndpoint } from './wfs/endpoint.js'; | ||
WfsFeatureTypeUniqueValue, | ||
WfsGetFeatureOptions, | ||
} from './wfs/model.js'; | ||
@@ -31,10 +32,16 @@ export { default as WmsEndpoint } from './wms/endpoint.js'; | ||
export type { | ||
Address, | ||
Contact, | ||
Provider, | ||
LayerStyle, | ||
BoundingBox, | ||
MetadataURL, | ||
FetchOptions, | ||
GenericEndpointInfo, | ||
MimeType, | ||
CrsCode, | ||
} from './shared/models.js'; | ||
export { default as OgcApiEndpoint } from './ogc-api/endpoint.js'; | ||
export * from './ogc-api/model.js'; | ||
export { useCache } from './shared/cache.js'; | ||
export { useCache, clearCache } from './shared/cache.js'; | ||
export { | ||
@@ -45,4 +52,8 @@ sharedFetch, | ||
} from './shared/http-utils.js'; | ||
export { | ||
check, | ||
default as ServiceExceptionError, | ||
} from './shared/service-exception-error.js'; | ||
export { enableFallbackWithoutWorker } from './worker/index.js'; | ||
import './worker-fallback/index.js'; |
import { | ||
_resetCache, | ||
clearCache, | ||
getCache, | ||
purgeEntries, | ||
purgeOutdatedEntries, | ||
readCacheEntry, | ||
@@ -25,3 +26,3 @@ setCacheExpiryDuration, | ||
NOW = 50000; | ||
await purgeEntries(); | ||
await purgeOutdatedEntries(); | ||
// set start time | ||
@@ -112,2 +113,15 @@ NOW = 10000; | ||
}); | ||
describe('clearCache', () => { | ||
let result; | ||
beforeEach(async () => { | ||
await storeCacheEntry({ old: true }, 'test', 'entry', '03'); | ||
NOW = 10800; | ||
await clearCache(); | ||
result = await useCache(factory, 'test', 'entry', '03'); | ||
}); | ||
it('do not use the cached function object', () => { | ||
expect(factory).toHaveBeenCalled(); | ||
expect(result).toEqual({ fresh: true }); | ||
}); | ||
}); | ||
}); | ||
@@ -114,0 +128,0 @@ |
@@ -100,3 +100,3 @@ let cacheExpiryDuration = 1000 * 60 * 60; // 1 day | ||
): Promise<T> { | ||
await purgeEntries(); | ||
await purgeOutdatedEntries(); | ||
if (await hasValidCacheEntry(...keys)) { | ||
@@ -122,3 +122,3 @@ return readCacheEntry(...keys); | ||
*/ | ||
export async function purgeEntries() { | ||
export async function purgeOutdatedEntries() { | ||
const cache = await getCache(); | ||
@@ -133,1 +133,13 @@ if (!cache) return; | ||
} | ||
/** | ||
* Remove all cache entries; will not prevent the creation of new ones | ||
*/ | ||
export async function clearCache() { | ||
const cache = await getCache(); | ||
if (!cache) return; | ||
const keys = await cache.keys(); | ||
for (const key of keys) { | ||
await cache.delete(key); | ||
} | ||
} |
@@ -8,2 +8,26 @@ /** | ||
export interface Address { | ||
deliveryPoint?: string; | ||
city?: string; | ||
administrativeArea?: string; | ||
postalCode?: string; | ||
country?: string; | ||
} | ||
export interface Contact { | ||
name?: string; | ||
organization?: string; | ||
position?: string; | ||
phone?: string; | ||
fax?: string; | ||
address?: Address; | ||
email?: string; | ||
} | ||
export interface Provider { | ||
name?: string; | ||
site?: string; | ||
contact?: Contact; | ||
} | ||
export type GenericEndpointInfo = { | ||
@@ -16,3 +40,18 @@ name: string; | ||
keywords: string[]; | ||
provider?: Provider; | ||
/** | ||
* Can contain the list of outputFormats from a WFS GetCapabilities, | ||
* or the list of 'Formats' from a WMS GetCapabilities | ||
*/ | ||
outputFormats?: MimeType[]; | ||
/** | ||
* Contains a list of formats that can be used for WMS GetFeatureInfo, | ||
* or undefined for other services such as WFS | ||
*/ | ||
infoFormats?: MimeType[]; | ||
/** | ||
* Contains a list of formats that can be used for Exceptions for WMS GetMap, | ||
* or undefined for other services such as WFS | ||
*/ | ||
exceptionFormats?: MimeType[] | string[]; | ||
}; | ||
@@ -31,2 +70,6 @@ | ||
export type OperationName = string; | ||
export type HttpMethod = 'Get' | 'Post'; | ||
export type OperationUrl = Partial<Record<HttpMethod, string>>; | ||
export interface LayerStyle { | ||
@@ -41,1 +84,7 @@ name: string; | ||
} | ||
export type MetadataURL = { | ||
format?: string; | ||
type?: string; | ||
url: string; | ||
}; |
@@ -13,2 +13,3 @@ import { parseXmlString } from '../shared/xml-utils.js'; | ||
readInfoFromCapabilities, | ||
readOperationUrlsFromCapabilities, | ||
readVersionFromCapabilities, | ||
@@ -44,2 +45,9 @@ } from './capabilities.js'; | ||
], | ||
metadata: [ | ||
{ | ||
format: 'text/plain', | ||
type: 'TC211', | ||
url: 'https://www.pigma.org/geonetwork/?uuid=cbcae9a4-7fc0-4fc8-bd78-089af3af4e8a', | ||
}, | ||
], | ||
name: 'asp:asp_rpg2010', | ||
@@ -64,2 +72,19 @@ otherCrs: ['EPSG:32615', 'EPSG:32616', 'EPSG:32617', 'EPSG:32618'], | ||
], | ||
metadata: [ | ||
{ | ||
format: 'text/html', | ||
type: 'TC211', | ||
url: 'https://www.pigma.org/geonetwork?uuid=4d840710-3f09-4f48-aa31-d2c4c0ee6fda', | ||
}, | ||
{ | ||
format: 'text/html', | ||
type: '19115', | ||
url: 'https://www.pigma.org/geonetwork?uuid=4d840710-3f09-4f48-aa31-d2c4c0ee6fda', | ||
}, | ||
{ | ||
format: 'text/xml', | ||
type: '19115', | ||
url: 'https://www.pigma.org/geonetwork/srv/fre/xml_iso19139?uuid=4d840710-3f09-4f48-aa31-d2c4c0ee6fda', | ||
}, | ||
], | ||
name: 'cd16:comptages_routiers_l', | ||
@@ -86,2 +111,19 @@ otherCrs: ['EPSG:32615', 'EPSG:32616', 'EPSG:32617', 'EPSG:32618'], | ||
name: 'cd16:hierarchisation_l', | ||
metadata: [ | ||
{ | ||
format: 'text/html', | ||
type: 'TC211', | ||
url: 'https://www.pigma.org/geonetwork?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
{ | ||
format: 'text/html', | ||
type: '19115', | ||
url: 'https://www.pigma.org/geonetwork?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
{ | ||
format: 'text/xml', | ||
type: '19115', | ||
url: 'https://www.pigma.org/geonetwork/srv/fre/xml_iso19139?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
], | ||
otherCrs: ['EPSG:32615', 'EPSG:32616', 'EPSG:32617', 'EPSG:32618'], | ||
@@ -99,3 +141,9 @@ outputFormats: [ | ||
const doc = parseXmlString(capabilities200); | ||
expect(readFeatureTypesFromCapabilities(doc)).toEqual(expectedTypes); | ||
const typesWithoutMetadataUrlAttributes = expectedTypes.map((type) => ({ | ||
...type, | ||
metadata: type.metadata.map((metadata) => ({ url: metadata.url })), | ||
})); | ||
expect(readFeatureTypesFromCapabilities(doc)).toEqual( | ||
typesWithoutMetadataUrlAttributes | ||
); | ||
}); | ||
@@ -147,2 +195,16 @@ it('reads the feature types (1.1.0)', () => { | ||
], | ||
metadata: [ | ||
{ | ||
url: 'https://www.geo2france.fr/geonetwork/srv/fre/catalog.search#/metadata/facf3747-bc19-44c7-9fd8-1f765d99c059', | ||
}, | ||
{ | ||
url: 'https://www.geo2france.fr/geonetwork/srv/fre/catalog.search#/metadata/facf3747-bc19-44c7-9fd8-1f765d99c059', | ||
}, | ||
{ | ||
url: 'https://www.geo2france.fr/geonetwork/srv/api/records/facf3747-bc19-44c7-9fd8-1f765d99c059/formatters/xml', | ||
}, | ||
{ | ||
url: 'https://www.geo2france.fr/geonetwork/srv/api/records/facf3747-bc19-44c7-9fd8-1f765d99c059/formatters/xml', | ||
}, | ||
], | ||
name: 'cr_hdf:domaine_public_hdf_com', | ||
@@ -186,2 +248,20 @@ otherCrs: [], | ||
}; | ||
const provider = { | ||
name: 'GIP ATGeRi', | ||
site: '', | ||
contact: { | ||
name: 'PIGMA', | ||
position: '', | ||
phone: '05.57.85.40.42', | ||
fax: '', | ||
address: { | ||
deliveryPoint: '', | ||
city: 'Bordeaux', | ||
administrativeArea: '', | ||
postalCode: '33075', | ||
country: '', | ||
}, | ||
email: 'admin.pigma@gipatgeri.fr', | ||
}, | ||
}; | ||
it('reads the service info (2.0.0)', () => { | ||
@@ -191,2 +271,3 @@ const doc = parseXmlString(capabilities200); | ||
...expectedInfo, | ||
provider, | ||
outputFormats: [ | ||
@@ -219,2 +300,3 @@ 'application/gml+xml; version=3.2', | ||
...expectedInfo, | ||
provider, | ||
outputFormats: [ | ||
@@ -262,2 +344,118 @@ 'text/xml; subtype=gml/3.1.1', | ||
}); | ||
describe('readOperationUrlsFromCapabilities', () => { | ||
it('reads the operation URLs (2.0.0)', () => { | ||
const doc = parseXmlString(capabilities200); | ||
const expectedUrls = { | ||
GetCapabilities: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
DescribeFeatureType: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetFeature: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetPropertyValue: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
ListStoredQueries: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
DescribeStoredQueries: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
CreateStoredQuery: { | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
DropStoredQuery: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
LockFeature: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetFeatureWithLock: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
Transaction: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
}; | ||
expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); | ||
}); | ||
it('reads the operation URLs (1.1.0)', () => { | ||
const doc = parseXmlString(capabilities110); | ||
const expectedUrls = { | ||
GetCapabilities: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
DescribeFeatureType: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetFeature: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetGmlObject: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
LockFeature: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetFeatureWithLock: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
Transaction: { | ||
Get: 'https://www.pigma.org/geoserver/wfs', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
}; | ||
expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); | ||
}); | ||
it('reads the operation URLs (1.0.0)', () => { | ||
const doc = parseXmlString(capabilities100); | ||
const expectedUrls = { | ||
GetCapabilities: { | ||
Get: 'https://www.pigma.org/geoserver/wfs?request=GetCapabilities', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
DescribeFeatureType: { | ||
Get: 'https://www.pigma.org/geoserver/wfs?request=DescribeFeatureType', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetFeature: { | ||
Get: 'https://www.pigma.org/geoserver/wfs?request=GetFeature', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
Transaction: { | ||
Get: 'https://www.pigma.org/geoserver/wfs?request=Transaction', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
LockFeature: { | ||
Get: 'https://www.pigma.org/geoserver/wfs?request=LockFeature', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
GetFeatureWithLock: { | ||
Get: 'https://www.pigma.org/geoserver/wfs?request=GetFeatureWithLock', | ||
Post: 'https://www.pigma.org/geoserver/wfs', | ||
}, | ||
}; | ||
expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); | ||
}); | ||
}); | ||
}); |
@@ -0,1 +1,2 @@ | ||
import { readProviderFromCapabilities } from '../shared/ows.js'; | ||
import { | ||
@@ -9,2 +10,3 @@ findChildElement, | ||
getRootElement, | ||
stripNamespace, | ||
} from '../shared/xml-utils.js'; | ||
@@ -17,2 +19,4 @@ import { simplifyEpsgUrn } from '../shared/crs-utils.js'; | ||
MimeType, | ||
type OperationName, | ||
type OperationUrl, | ||
} from '../shared/models.js'; | ||
@@ -22,2 +26,35 @@ import { WfsFeatureTypeInternal, WfsVersion } from './model.js'; | ||
/** | ||
* Will read the operation URLS from the capabilities doc | ||
* @param capabilitiesDoc Capabilities document | ||
*/ | ||
export function readOperationUrlsFromCapabilities( | ||
capabilitiesDoc: XmlDocument | ||
): Record<OperationName, OperationUrl> { | ||
const urls: Record<OperationName, OperationUrl> = {}; | ||
const capabilities = getRootElement(capabilitiesDoc); | ||
const operationsMetadata = findChildElement( | ||
capabilities, | ||
'OperationsMetadata' | ||
); | ||
if (operationsMetadata) { | ||
// WFS 1.1.0 or 2.0.0 | ||
findChildrenElement(operationsMetadata, 'Operation').forEach( | ||
(operation) => { | ||
const name = getElementAttribute(operation, 'name'); | ||
urls[name] = parseOperation110(operation); | ||
} | ||
); | ||
} else { | ||
// WFS 1.0.0 | ||
const capability = findChildElement(capabilities, 'Capability'); | ||
const request = findChildElement(capability, 'Request'); | ||
getChildrenElement(request).forEach((operation) => { | ||
const name = stripNamespace(getElementName(operation)); | ||
urls[name] = parseOperation100(operation); | ||
}); | ||
} | ||
return urls; | ||
} | ||
/** | ||
* Will read a WFS version from the capabilities doc | ||
@@ -97,2 +134,7 @@ * @param capabilitiesDoc Capabilities document | ||
} | ||
let provider; | ||
// no provider information defined in capabilities for WFS 1.0.0 | ||
if (version !== '1.0.0') { | ||
provider = readProviderFromCapabilities(capabilitiesDoc); | ||
} | ||
@@ -106,2 +148,3 @@ return { | ||
keywords, | ||
provider, | ||
outputFormats: readOutputFormatsFromCapabilities(capabilitiesDoc), | ||
@@ -129,2 +172,34 @@ }; | ||
/** | ||
* Parse an operation definition from a WFS 1.0.0 capabilities (e.g. GetFeature) | ||
* @param operation Operation element | ||
*/ | ||
function parseOperation100(operation: XmlElement): OperationUrl { | ||
const urls: OperationUrl = {}; | ||
const dcpType = findChildrenElement(operation, 'DCPType'); | ||
const http = dcpType.flatMap((d) => findChildrenElement(d, 'HTTP')); | ||
const methods = http.flatMap((h) => getChildrenElement(h)); | ||
methods.forEach((method) => { | ||
const methodName = stripNamespace(getElementName(method)); | ||
urls[methodName] = getElementAttribute(method, 'onlineResource'); | ||
}); | ||
return urls; | ||
} | ||
/** | ||
* Parse an operation definition from a WFS 1.1+ capabilities (e.g. GetFeature) | ||
* @param operation Operation element | ||
*/ | ||
function parseOperation110(operation: XmlElement): OperationUrl { | ||
const urls: OperationUrl = {}; | ||
const dcpType = findChildrenElement(operation, 'DCP'); | ||
const http = dcpType.flatMap((d) => findChildElement(d, 'HTTP')); | ||
const methods = http.flatMap((h) => getChildrenElement(h)); | ||
methods.forEach((method) => { | ||
const methodName = stripNamespace(getElementName(method)); | ||
urls[methodName] = getElementAttribute(method, 'xlink:href'); | ||
}); | ||
return urls; | ||
} | ||
/** | ||
* Parse a feature type in a capabilities doc | ||
@@ -177,2 +252,18 @@ */ | ||
.filter((v, i, arr) => arr.indexOf(v) === i); | ||
const metadata = | ||
serviceVersion === '2.0.0' | ||
? findChildrenElement(featureTypeEl, 'MetadataURL').map( | ||
(metadataUrlEl) => ({ | ||
url: getElementAttribute(metadataUrlEl, 'xlink:href'), | ||
}) | ||
) | ||
: findChildrenElement(featureTypeEl, 'MetadataURL').map( | ||
(metadataUrlEl) => ({ | ||
format: getElementAttribute(metadataUrlEl, 'format'), | ||
type: getElementAttribute(metadataUrlEl, 'type'), | ||
url: getElementText(metadataUrlEl).trim(), | ||
}) | ||
); | ||
return { | ||
@@ -192,3 +283,4 @@ name: getElementText(findChildElement(featureTypeEl, 'Name')), | ||
keywords: keywords, | ||
...(metadata.length && { metadata }), | ||
}; | ||
} |
@@ -11,2 +11,4 @@ // @ts-expect-error ts-migrate(7016) | ||
import capabilitiesStates from '../../fixtures/wfs/capabilities-states-2-0-0.xml'; | ||
// @ts-expect-error ts-migrate(7016) | ||
import exceptionReportWms from '../../fixtures/wfs/exception-report-wms.xml'; | ||
import WfsEndpoint from './endpoint.js'; | ||
@@ -75,2 +77,14 @@ import { useCache } from '../shared/cache.js'; | ||
}); | ||
describe('service exception handling', () => { | ||
beforeEach(() => { | ||
global.fetchResponseFactory = () => exceptionReportWms; | ||
endpoint = new WfsEndpoint('https://my.test.service/ogc/wms'); | ||
}); | ||
it('rejects when the endpoint returns an exception report', async () => { | ||
await expect(endpoint.isReady()).rejects.toThrow( | ||
'msWFSDispatch(): WFS server error. WFS request not enabled. Check wfs/ows_enable_request settings.' | ||
); | ||
}); | ||
}); | ||
}); | ||
@@ -137,2 +151,13 @@ | ||
keywords: ['features', 'hierarchisation_l'], | ||
metadata: [ | ||
{ | ||
url: 'https://www.pigma.org/geonetwork?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
{ | ||
url: 'https://www.pigma.org/geonetwork?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
{ | ||
url: 'https://www.pigma.org/geonetwork/srv/fre/xml_iso19139?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
], | ||
otherCrs: ['EPSG:32615', 'EPSG:32616', 'EPSG:32617', 'EPSG:32618'], | ||
@@ -178,2 +203,13 @@ outputFormats: [ | ||
keywords: ['features', 'hierarchisation_l'], | ||
metadata: [ | ||
{ | ||
url: 'https://www.pigma.org/geonetwork?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
{ | ||
url: 'https://www.pigma.org/geonetwork?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
{ | ||
url: 'https://www.pigma.org/geonetwork/srv/fre/xml_iso19139?uuid=cd27adaa-0ec5-4934-9374-143df09fb9f6', | ||
}, | ||
], | ||
otherCrs: ['EPSG:32615', 'EPSG:32616', 'EPSG:32617', 'EPSG:32618'], | ||
@@ -233,2 +269,20 @@ outputFormats: [ | ||
keywords: ['WFS', 'WMS', 'GEOSERVER'], | ||
provider: { | ||
name: 'GIP ATGeRi', | ||
site: '', | ||
contact: { | ||
name: 'PIGMA', | ||
position: '', | ||
phone: '05.57.85.40.42', | ||
fax: '', | ||
address: { | ||
deliveryPoint: '', | ||
city: 'Bordeaux', | ||
administrativeArea: '', | ||
postalCode: '33075', | ||
country: '', | ||
}, | ||
email: 'admin.pigma@gipatgeri.fr', | ||
}, | ||
}, | ||
outputFormats: [ | ||
@@ -453,3 +507,3 @@ 'application/gml+xml; version=3.2', | ||
).toEqual( | ||
'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&OUTPUTFORMAT=application%2Fgml%2Bxml%3B+version%3D3.2&COUNT=200' | ||
'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&OUTPUTFORMAT=application%2Fgml%2Bxml%3B+version%3D3.2&COUNT=200' | ||
); | ||
@@ -463,3 +517,3 @@ }); | ||
).toEqual( | ||
'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Acomptages_routiers_l&OUTPUTFORMAT=application%2Fjson' | ||
'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Acomptages_routiers_l&OUTPUTFORMAT=application%2Fjson' | ||
); | ||
@@ -474,3 +528,3 @@ }); | ||
).toEqual( | ||
'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&SRSNAME=EPSG%3A2154&BBOX=1%2C2%2C3%2C4' | ||
'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&SRSNAME=EPSG%3A2154&BBOX=1%2C2%2C3%2C4' | ||
); | ||
@@ -499,2 +553,20 @@ }); | ||
}); | ||
it('returns a GetFeature url with the desired attributes', () => { | ||
expect( | ||
endpoint.getFeatureUrl('hierarchisation_l', { | ||
attributes: ['field1', 'field2'], | ||
}) | ||
).toEqual( | ||
'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&PROPERTYNAME=field1%2Cfield2' | ||
); | ||
}); | ||
it('returns a GetFeature url with only the hit count', () => { | ||
expect( | ||
endpoint.getFeatureUrl('hierarchisation_l', { | ||
hitsOnly: true, | ||
}) | ||
).toEqual( | ||
'https://www.pigma.org/geoserver/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=cd16%3Ahierarchisation_l&RESULTTYPE=hits&COUNT=1' | ||
); | ||
}); | ||
}); | ||
@@ -525,2 +597,37 @@ | ||
}); | ||
describe('#getCapabilitiesUrl', () => { | ||
it.skip('returns the URL used for the request before the capabilities are retrieved', async () => { | ||
expect(endpoint.getCapabilitiesUrl()).toBe( | ||
'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetCapabilities' | ||
); | ||
await endpoint.isReady(); | ||
}); | ||
it('returns the self-reported URL after the capabilities are retrieved', async () => { | ||
await endpoint.isReady(); | ||
expect(endpoint.getCapabilitiesUrl()).toBe( | ||
'https://www.pigma.org/geoserver/wfs?SERVICE=WMS&REQUEST=GetCapabilities' | ||
); | ||
}); | ||
}); | ||
describe('#getOperationUrl', () => { | ||
it.skip('returns NULL before the document is loaded', async () => { | ||
expect(endpoint.getOperationUrl('GetMap')).toBeNull(); | ||
await endpoint.isReady(); | ||
}); | ||
it('returns undefined for a non-existant operation', async () => { | ||
await endpoint.isReady(); | ||
expect(endpoint.getOperationUrl('foo')).toBeUndefined(); | ||
}); | ||
it('returns the correct URL for an existant operation', async () => { | ||
await endpoint.isReady(); | ||
expect(endpoint.getOperationUrl('GetFeature')).toBe( | ||
'https://www.pigma.org/geoserver/wfs' | ||
); | ||
}); | ||
}); | ||
}); |
@@ -14,6 +14,6 @@ import { | ||
import { | ||
BoundingBox, | ||
CrsCode, | ||
GenericEndpointInfo, | ||
MimeType, | ||
type HttpMethod, | ||
type OperationName, | ||
type OperationUrl, | ||
} from '../shared/models.js'; | ||
@@ -24,2 +24,3 @@ import { | ||
WfsFeatureTypeSummary, | ||
WfsGetFeatureOptions, | ||
WfsVersion, | ||
@@ -37,2 +38,3 @@ } from './model.js'; | ||
private _featureTypes: WfsFeatureTypeInternal[] | null; | ||
private _url: Record<OperationName, OperationUrl>; | ||
private _version: WfsVersion | null; | ||
@@ -59,5 +61,6 @@ | ||
this._capabilitiesUrl | ||
).then(({ info, featureTypes, version }) => { | ||
).then(({ info, featureTypes, url, version }) => { | ||
this._info = info; | ||
this._featureTypes = featureTypes; | ||
this._url = url; | ||
this._version = version; | ||
@@ -132,2 +135,3 @@ }); | ||
keywords: featureType.keywords, | ||
...('metadata' in featureType && { metadata: featureType.metadata }), | ||
} as WfsFeatureTypeSummary; | ||
@@ -149,3 +153,3 @@ } | ||
const describeUrl = generateDescribeFeatureTypeUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl('DescribeFeatureType'), | ||
this._version, | ||
@@ -155,3 +159,3 @@ name | ||
const getFeatureUrl = generateGetFeatureUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl('GetFeature'), | ||
this._version, | ||
@@ -257,24 +261,6 @@ name, | ||
* @param featureType | ||
* @param {Object} [options] | ||
* @property [options.maxFeatures] no limit if undefined | ||
* @property [options.asJson] if true, will ask for GeoJSON; will throw if the service does not support it | ||
* @property [options.outputFormat] a supported output format (overridden by `asJson`) | ||
* @property [options.outputCrs] if unspecified, this will be the data native projection | ||
* @property [options.extent] an extent to restrict returned objects | ||
* @property [options.extentCrs] if unspecified, `extent` should be in the data native projection | ||
* @property [options.startIndex] if the service supports it, this will be the index of the first feature to return | ||
* @param options | ||
* @returns Returns null if endpoint is not ready | ||
*/ | ||
getFeatureUrl( | ||
featureType: string, | ||
options: { | ||
maxFeatures?: number; | ||
asJson?: boolean; | ||
outputFormat?: MimeType; | ||
outputCrs?: CrsCode; | ||
extent?: BoundingBox; | ||
extentCrs?: CrsCode; | ||
startIndex?: number; | ||
} | ||
) { | ||
getFeatureUrl(featureType: string, options?: WfsGetFeatureOptions) { | ||
if (!this._featureTypes) { | ||
@@ -291,2 +277,4 @@ return null; | ||
startIndex, | ||
attributes, | ||
hitsOnly, | ||
} = options || {}; | ||
@@ -317,3 +305,3 @@ const internalFeatureType = this._getFeatureTypeByName(featureType); | ||
return generateGetFeatureUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl('GetFeature'), | ||
this._version, | ||
@@ -323,4 +311,4 @@ internalFeatureType.name, | ||
maxFeatures, | ||
undefined, | ||
undefined, | ||
attributes, | ||
hitsOnly, | ||
outputCrs, | ||
@@ -332,2 +320,31 @@ extent, | ||
} | ||
/** | ||
* Returns the Capabilities URL of the WMS | ||
* | ||
* This is the URL reported by the service if available, otherwise the URL | ||
* passed to the constructor | ||
*/ | ||
getCapabilitiesUrl() { | ||
const baseUrl = this.getOperationUrl('GetCapabilities'); | ||
if (!baseUrl) { | ||
return this._capabilitiesUrl; | ||
} | ||
return setQueryParams(baseUrl, { | ||
SERVICE: 'WMS', | ||
REQUEST: 'GetCapabilities', | ||
}); | ||
} | ||
/** | ||
* Returns the URL reported by the WFS for the given operation | ||
* @param operationName e.g. GetFeature, GetCapabilities, etc. | ||
* @param method HTTP method | ||
*/ | ||
getOperationUrl(operationName: OperationName, method: HttpMethod = 'Get') { | ||
if (!this._url) { | ||
return null; | ||
} | ||
return this._url[operationName]?.[method]; | ||
} | ||
} |
@@ -34,2 +34,3 @@ import { | ||
keywords, | ||
metadata, | ||
} = featureType; | ||
@@ -51,3 +52,3 @@ | ||
const properties = typeElementsEls | ||
.filter((el) => getElementAttribute(el, 'type').startsWith('xsd:')) | ||
.filter((el) => /^xsd:|^xs:/.test(getElementAttribute(el, 'type'))) | ||
.reduce( | ||
@@ -84,2 +85,3 @@ (prev, curr) => ({ | ||
...(keywords && { keywords }), | ||
...(metadata && { metadata }), | ||
}; | ||
@@ -86,0 +88,0 @@ } |
@@ -1,2 +0,7 @@ | ||
import { BoundingBox, CrsCode, MimeType } from '../shared/models.js'; | ||
import { | ||
BoundingBox, | ||
CrsCode, | ||
MetadataURL, | ||
MimeType, | ||
} from '../shared/models.js'; | ||
@@ -14,2 +19,3 @@ export type WfsVersion = '1.0.0' | '1.1.0' | '2.0.0'; | ||
keywords?: string[]; | ||
metadata?: MetadataURL[]; | ||
}; | ||
@@ -50,2 +56,3 @@ | ||
keywords?: string[]; | ||
metadata?: MetadataURL[]; | ||
}; | ||
@@ -107,1 +114,42 @@ | ||
>; | ||
export type WfsGetFeatureOptions = { | ||
/** | ||
* No limit if undefined | ||
*/ | ||
maxFeatures?: number; | ||
/** | ||
* if true, will ask for GeoJSON; will throw if the service does not support it | ||
*/ | ||
asJson?: boolean; | ||
/** | ||
* a supported output format (overridden by `asJson`) | ||
*/ | ||
outputFormat?: MimeType; | ||
/** | ||
* if unspecified, this will be the data native projection | ||
*/ | ||
outputCrs?: CrsCode; | ||
/** | ||
* an extent to restrict returned objects | ||
*/ | ||
extent?: BoundingBox; | ||
/** | ||
* if unspecified, `extent` should be in the data native projection | ||
*/ | ||
extentCrs?: CrsCode; | ||
/** | ||
* if the service supports it, this will be the index of the first feature to return | ||
*/ | ||
startIndex?: number; | ||
/** | ||
* if not defined, all attributes will be included; note that the fact | ||
* that these attributes exist or not will not be checked! | ||
*/ | ||
attributes?: string[]; | ||
/** | ||
* if true, will not return feature data, only hit count | ||
* note: this might not work for WFS version < 2 | ||
*/ | ||
hitsOnly?: boolean; | ||
}; |
import { | ||
readInfoFromCapabilities, | ||
readLayersFromCapabilities, | ||
readOperationUrlsFromCapabilities, | ||
readVersionFromCapabilities, | ||
@@ -11,2 +12,3 @@ } from './capabilities.js'; | ||
import { parseXmlString } from '../shared/xml-utils.js'; | ||
import type { WmsLayerFull } from './model.js'; | ||
@@ -136,2 +138,11 @@ describe('WMS capabilities', () => { | ||
keywords: ['Geologie', 'INSPIRE:Geology', 'Geology'], | ||
maxScaleDenominator: 1e7, | ||
minScaleDenominator: 200000, | ||
metadata: [ | ||
{ | ||
format: 'text/xml', | ||
type: 'TC211', | ||
url: 'http://www.geocatalogue.fr/api-public/servicesRest?Service=CSW&Request=GetRecordById&Version=2.0.2&id=BR_CAR_ADA&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full', | ||
}, | ||
], | ||
name: 'SCAN_F_GEOL1M', | ||
@@ -190,3 +201,12 @@ queryable: false, | ||
keywords: ['Geologie', 'INSPIRE:Geology', 'Geology'], | ||
maxScaleDenominator: 500000, | ||
minScaleDenominator: 80000, | ||
name: 'SCAN_F_GEOL250', | ||
metadata: [ | ||
{ | ||
format: 'text/xml', | ||
type: 'TC211', | ||
url: 'http://www.geocatalogue.fr/api-public/servicesRest?Service=CSW&Request=GetRecordById&Version=2.0.2&id=BR_CAR_ACA&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full', | ||
}, | ||
], | ||
queryable: true, | ||
@@ -231,3 +251,12 @@ opaque: true, | ||
keywords: ['Geologie', 'INSPIRE:Geology', 'Geology'], | ||
maxScaleDenominator: 251000, | ||
minScaleDenominator: 9000, | ||
name: 'SCAN_D_GEOL50', | ||
metadata: [ | ||
{ | ||
format: 'text/xml', | ||
type: 'TC211', | ||
url: 'http://www.geocatalogue.fr/api-public/servicesRest?Service=CSW&Request=GetRecordById&Version=2.0.2&id=72cc8d40-1bb6-41a3-8376-9734f23336ff&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full', | ||
}, | ||
], | ||
queryable: true, | ||
@@ -237,2 +266,45 @@ opaque: true, | ||
title: 'Carte géologique image de la France au 1/50 000e', | ||
children: [ | ||
{ | ||
abstract: '', | ||
attribution, | ||
availableCrs: [ | ||
'EPSG:4326', | ||
'EPSG:3857', | ||
'CRS:84', | ||
'EPSG:32620', | ||
'EPSG:32621', | ||
], | ||
boundingBoxes: { | ||
'CRS:84': ['-12.2064', '40.681', '11.894', '52.1672'], | ||
'EPSG:32620': [ | ||
'3.88148e+06', | ||
'6.13796e+06', | ||
'6.31307e+06', | ||
'8.70752e+06', | ||
], | ||
'EPSG:32621': [ | ||
'3.52434e+06', | ||
'5.74736e+06', | ||
'5.97375e+06', | ||
'8.23867e+06', | ||
], | ||
'EPSG:3857': [ | ||
'-1.35881e+06', | ||
'4.96541e+06', | ||
'1.32403e+06', | ||
'6.83041e+06', | ||
], | ||
'EPSG:4326': ['-12.2064', '40.681', '11.894', '52.1672'], | ||
}, | ||
keywords: [], | ||
maxScaleDenominator: 251000, | ||
minScaleDenominator: 9000, | ||
name: 'INHERIT_SCALE', | ||
queryable: false, | ||
opaque: false, | ||
styles, | ||
title: 'Inherited scale denominators', | ||
}, | ||
], | ||
}, | ||
@@ -283,3 +355,5 @@ { | ||
const doc = parseXmlString(capabilities111); | ||
expect(readLayersFromCapabilities(doc)).toEqual(expectedLayers); | ||
expect( | ||
readLayersFromCapabilities(doc).map(fixupScaleDenominators) | ||
).toEqual(expectedLayers); | ||
}); | ||
@@ -296,2 +370,16 @@ }); | ||
title: 'GéoServices : géologie, hydrogéologie et gravimétrie', | ||
outputFormats: [ | ||
'image/png', | ||
'image/gif', | ||
'image/jpeg', | ||
'image/ecw', | ||
'image/tiff', | ||
'image/png; mode=8bit', | ||
'application/x-pdf', | ||
'image/svg+xml', | ||
], | ||
infoFormats: ['text/plain', 'application/vnd.ogc.gml'], | ||
exceptionFormats: [ | ||
/* these differ depending on the WMS version used */ | ||
], | ||
keywords: [ | ||
@@ -306,2 +394,19 @@ 'Géologie', | ||
], | ||
provider: { | ||
contact: { | ||
name: 'Support BRGM', | ||
organization: 'BRGM', | ||
position: 'pointOfContact', | ||
phone: '+33(0)2 38 64 34 34', | ||
fax: '+33(0)2 38 64 35 18', | ||
address: { | ||
deliveryPoint: '3, Avenue Claude Guillemin, BP36009', | ||
city: 'Orléans', | ||
administrativeArea: 'Centre', | ||
postalCode: '45060', | ||
country: 'France', | ||
}, | ||
email: 'contact-brgm@brgm.fr', | ||
}, | ||
}, | ||
}; | ||
@@ -311,2 +416,3 @@ | ||
const doc = parseXmlString(capabilities130); | ||
expectedInfo.exceptionFormats = ['XML', 'INIMAGE', 'BLANK']; | ||
expect(readInfoFromCapabilities(doc)).toEqual(expectedInfo); | ||
@@ -317,5 +423,64 @@ }); | ||
const doc = parseXmlString(capabilities111); | ||
expectedInfo.exceptionFormats = [ | ||
'application/vnd.ogc.se_xml', | ||
'application/vnd.ogc.se_inimage', | ||
'application/vnd.ogc.se_blank', | ||
]; | ||
expect(readInfoFromCapabilities(doc)).toEqual(expectedInfo); | ||
}); | ||
}); | ||
describe('readOperationUrlsFromCapabilities', () => { | ||
const expectedUrls = { | ||
GetCapabilities: { | ||
Get: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
Post: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
}, | ||
GetMap: { | ||
Get: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
Post: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
}, | ||
GetFeatureInfo: { | ||
Get: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
Post: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
}, | ||
DescribeLayer: { | ||
Get: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
Post: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
}, | ||
GetLegendGraphic: { | ||
Get: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
Post: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
}, | ||
GetStyles: { | ||
Get: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
Post: 'http://geoservices.brgm.fr/geologie?language=fre&', | ||
}, | ||
}; | ||
it('reads the operations URLs (1.3.0)', () => { | ||
const doc = parseXmlString(capabilities130); | ||
expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); | ||
}); | ||
it('reads the operations URLs (1.1.1)', () => { | ||
const doc = parseXmlString(capabilities111); | ||
expect(readOperationUrlsFromCapabilities(doc)).toEqual(expectedUrls); | ||
}); | ||
}); | ||
}); | ||
/** | ||
* Round scale denominators to avoid problems with floating point precision | ||
* @param layer | ||
*/ | ||
function fixupScaleDenominators(layer: WmsLayerFull): WmsLayerFull { | ||
if (layer.minScaleDenominator !== undefined) { | ||
layer.minScaleDenominator = Math.round(layer.minScaleDenominator); | ||
} | ||
if (layer.maxScaleDenominator !== undefined) { | ||
layer.maxScaleDenominator = Math.round(layer.maxScaleDenominator); | ||
} | ||
layer.children?.forEach(fixupScaleDenominators); | ||
return layer; | ||
} |
@@ -0,19 +1,46 @@ | ||
import { XmlDocument, XmlElement } from '@rgrove/parse-xml'; | ||
import { hasInvertedCoordinates } from '../shared/crs-utils.js'; | ||
import { | ||
BoundingBox, | ||
CrsCode, | ||
GenericEndpointInfo, | ||
LayerStyle, | ||
type Provider, | ||
type OperationName, | ||
type OperationUrl, | ||
} from '../shared/models.js'; | ||
import { | ||
findChildElement, | ||
findChildrenElement, | ||
getChildrenElement, | ||
getElementAttribute, | ||
getElementName, | ||
getElementText, | ||
getRootElement, | ||
stripNamespace, | ||
} from '../shared/xml-utils.js'; | ||
import { hasInvertedCoordinates } from '../shared/crs-utils.js'; | ||
import { XmlDocument, XmlElement } from '@rgrove/parse-xml'; | ||
import { | ||
BoundingBox, | ||
CrsCode, | ||
GenericEndpointInfo, | ||
LayerStyle, | ||
} from '../shared/models.js'; | ||
import { WmsLayerAttribution, WmsLayerFull, WmsVersion } from './model.js'; | ||
/** | ||
* Will read all operation URLs from the capabilities doc | ||
* @param capabilitiesDoc Capabilities document | ||
* @return The parsed operations URLs | ||
*/ | ||
export function readOperationUrlsFromCapabilities( | ||
capabilitiesDoc: XmlDocument | ||
) { | ||
const urls: Record<OperationName, OperationUrl> = {}; | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
'Capability' | ||
); | ||
const request = findChildElement(capability, 'Request'); | ||
getChildrenElement(request).forEach((operation) => { | ||
const operationName = stripNamespace(getElementName(operation)); | ||
urls[operationName] = parseOperation(operation); | ||
}); | ||
return urls; | ||
} | ||
/** | ||
* Will read a WMS version from the capabilities doc | ||
@@ -43,3 +70,54 @@ * @param capabilitiesDoc Capabilities document | ||
export function readOutputFormatsFromCapabilities( | ||
capabilitiesDoc: XmlDocument | ||
) { | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
'Capability' | ||
); | ||
const getMap = findChildElement( | ||
findChildElement(capability, 'Request'), | ||
'GetMap' | ||
); | ||
const outputFormats = findChildrenElement(getMap, 'Format').map( | ||
getElementText | ||
); | ||
return outputFormats; | ||
} | ||
export function readInfoFormatsFromCapabilities(capabilitiesDoc: XmlDocument) { | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
'Capability' | ||
); | ||
const getFeatureInfo = findChildElement( | ||
findChildElement(capability, 'Request'), | ||
'GetFeatureInfo' | ||
); | ||
const outputFormats = findChildrenElement(getFeatureInfo, 'Format').map( | ||
getElementText | ||
); | ||
return outputFormats; | ||
} | ||
/** | ||
* Will return all available exception formats | ||
* @param capabilitiesDoc Capabiliites document | ||
* @return Available exception formats | ||
*/ | ||
export function readExceptionFormatsFromCapabilities( | ||
capabilitiesDoc: XmlDocument | ||
) { | ||
const capability = findChildElement( | ||
getRootElement(capabilitiesDoc), | ||
'Capability' | ||
); | ||
const exception = findChildElement(capability, 'Exception'); | ||
const exceptionFormats = findChildrenElement(exception, 'Format').map( | ||
getElementText | ||
); | ||
return exceptionFormats; | ||
} | ||
/** | ||
* Will read service-related info from the capabilities doc | ||
@@ -53,2 +131,6 @@ * @param capabilitiesDoc Capabilities document | ||
const service = findChildElement(getRootElement(capabilitiesDoc), 'Service'); | ||
const outputFormats = readOutputFormatsFromCapabilities(capabilitiesDoc); | ||
const infoFormats = readInfoFormatsFromCapabilities(capabilitiesDoc); | ||
const exceptionFormats = | ||
readExceptionFormatsFromCapabilities(capabilitiesDoc); | ||
const keywords = findChildrenElement( | ||
@@ -60,2 +142,3 @@ findChildElement(service, 'KeywordList'), | ||
.filter((v, i, arr) => arr.indexOf(v) === i); | ||
const provider = readProviderFromCapabilities(capabilitiesDoc); | ||
@@ -66,4 +149,8 @@ return { | ||
abstract: getElementText(findChildElement(service, 'Abstract')), | ||
outputFormats: outputFormats, | ||
infoFormats: infoFormats, | ||
exceptionFormats: exceptionFormats, | ||
fees: getElementText(findChildElement(service, 'Fees')), | ||
constraints: getElementText(findChildElement(service, 'AccessConstraints')), | ||
provider, | ||
keywords, | ||
@@ -74,2 +161,19 @@ }; | ||
/** | ||
* Parse an operation definition from a WMS capabilities (e.g. GetMap) | ||
* @param operation Operation element | ||
*/ | ||
function parseOperation(operation: XmlElement): OperationUrl { | ||
const urls: OperationUrl = {}; | ||
const dcpType = findChildrenElement(operation, 'DCPType'); | ||
const http = dcpType.flatMap((d) => findChildElement(d, 'HTTP')); | ||
const methods = http.flatMap((h) => getChildrenElement(h)); | ||
methods.forEach((method) => { | ||
const onlineResource = findChildElement(method, 'OnlineResource'); | ||
const methodName = stripNamespace(getElementName(method)); | ||
urls[methodName] = getElementAttribute(onlineResource, 'xlink:href'); | ||
}); | ||
return urls; | ||
} | ||
/** | ||
* Parse a layer in a capabilities doc | ||
@@ -83,3 +187,5 @@ */ | ||
inheritedAttribution: WmsLayerAttribution = null, | ||
inheritedBoundingBoxes: Record<CrsCode, BoundingBox> = null | ||
inheritedBoundingBoxes: Record<CrsCode, BoundingBox> = null, | ||
inheritedMaxScaleDenom: number = null, | ||
inheritedMinScaleDenom: number = null | ||
): WmsLayerFull { | ||
@@ -114,2 +220,26 @@ const srsTag = version === '1.3.0' ? 'CRS' : 'SRS'; | ||
} | ||
function parseScaleHintValue(textValue, defaultValue) { | ||
if (textValue === '') { | ||
return defaultValue; | ||
} | ||
// convert resolution to scale denominator using the common pixel size of | ||
// 0.28×0.28 mm as defined in WMS 1.3.0 specification section 7.2.4.6.9 | ||
return Math.sqrt(0.5 * parseFloat(textValue) ** 2) / 0.00028; | ||
} | ||
function parseScaleHint() { | ||
const scaleHint = findChildElement(layerEl, 'ScaleHint'); | ||
if (!scaleHint) { | ||
return [inheritedMinScaleDenom, inheritedMaxScaleDenom]; | ||
} | ||
const min = getElementAttribute(scaleHint, 'min'); | ||
const max = getElementAttribute(scaleHint, 'max'); | ||
return [ | ||
parseScaleHintValue(min, inheritedMinScaleDenom), | ||
parseScaleHintValue(max, inheritedMaxScaleDenom), | ||
]; | ||
} | ||
function parseScaleDenominator(name, inheritedValue) { | ||
const textValue = getElementText(findChildElement(layerEl, name)); | ||
return textValue === '' ? inheritedValue : parseFloat(textValue); | ||
} | ||
const attributionEl = findChildElement(layerEl, 'Attribution'); | ||
@@ -160,4 +290,38 @@ const attribution = | ||
let minScaleDenominator, maxScaleDenominator; | ||
if (version === '1.3.0') { | ||
minScaleDenominator = parseScaleDenominator( | ||
'MinScaleDenominator', | ||
inheritedMinScaleDenom | ||
); | ||
maxScaleDenominator = parseScaleDenominator( | ||
'MaxScaleDenominator', | ||
inheritedMaxScaleDenom | ||
); | ||
} else { | ||
[minScaleDenominator, maxScaleDenominator] = parseScaleHint(); | ||
} | ||
const metadata = findChildrenElement(layerEl, 'MetadataURL').map( | ||
(metadataUrlEl) => ({ | ||
type: getElementAttribute(metadataUrlEl, 'type'), | ||
format: getElementText(findChildElement(metadataUrlEl, 'Format')), | ||
url: getElementAttribute( | ||
findChildElement(metadataUrlEl, 'OnlineResource'), | ||
'xlink:href' | ||
), | ||
}) | ||
); | ||
const children = findChildrenElement(layerEl, 'Layer').map((layer) => | ||
parseLayer(layer, version, availableCrs, styles, attribution, boundingBoxes) | ||
parseLayer( | ||
layer, | ||
version, | ||
availableCrs, | ||
styles, | ||
attribution, | ||
boundingBoxes, | ||
maxScaleDenominator, | ||
minScaleDenominator | ||
) | ||
); | ||
@@ -175,2 +339,5 @@ return { | ||
opaque, | ||
...(minScaleDenominator !== null ? { minScaleDenominator } : {}), | ||
...(maxScaleDenominator !== null ? { maxScaleDenominator } : {}), | ||
...(metadata.length && { metadata }), | ||
...(children.length && { children }), | ||
@@ -213,1 +380,46 @@ }; | ||
} | ||
/** | ||
* Read provider information from capabilities | ||
* @param capabilitiesDoc | ||
*/ | ||
function readProviderFromCapabilities(capabilitiesDoc: XmlDocument): Provider { | ||
const service = findChildElement(getRootElement(capabilitiesDoc), 'Service'); | ||
const contactInformation = findChildElement(service, 'ContactInformation'); | ||
const contactPersonPrimary = findChildElement( | ||
contactInformation, | ||
'ContactPersonPrimary' | ||
); | ||
const address = findChildElement(contactInformation, 'ContactAddress'); | ||
return { | ||
contact: { | ||
name: getElementText( | ||
findChildElement(contactPersonPrimary, 'ContactPerson') | ||
), | ||
organization: getElementText( | ||
findChildElement(contactPersonPrimary, 'ContactOrganization') | ||
), | ||
position: getElementText( | ||
findChildElement(contactInformation, 'ContactPosition') | ||
), | ||
phone: getElementText( | ||
findChildElement(contactInformation, 'ContactVoiceTelephone') | ||
), | ||
fax: getElementText( | ||
findChildElement(contactInformation, 'ContactFacsimileTelephone') | ||
), | ||
address: { | ||
deliveryPoint: getElementText(findChildElement(address, 'Address')), | ||
city: getElementText(findChildElement(address, 'City')), | ||
administrativeArea: getElementText( | ||
findChildElement(address, 'StateOrProvince') | ||
), | ||
postalCode: getElementText(findChildElement(address, 'PostCode')), | ||
country: getElementText(findChildElement(address, 'Country')), | ||
}, | ||
email: getElementText( | ||
findChildElement(contactInformation, 'ContactElectronicMailAddress') | ||
), | ||
}, | ||
}; | ||
} |
@@ -5,2 +5,4 @@ // @ts-expect-error ts-migrate(7016) | ||
import capabilitiesStates from '../../fixtures/wms/capabilities-states-1-3-0.xml'; | ||
// @ts-expect-error ts-migrate(7016) | ||
import exceptionReportWfs from '../../fixtures/wms/service-exception-report-wfs.xml'; | ||
import WmsEndpoint from './endpoint.js'; | ||
@@ -56,2 +58,13 @@ import { useCache } from '../shared/cache.js'; | ||
}); | ||
describe('service exception handling', () => { | ||
beforeEach(() => { | ||
global.fetchResponseFactory = () => exceptionReportWfs; | ||
endpoint = new WmsEndpoint('https://my.test.service/ogc/wfs'); | ||
}); | ||
it('rejects when the endpoint returns an exception report', async () => { | ||
await expect(endpoint.isReady()).rejects.toThrow( | ||
'msWMSGetCapabilities(): WMS server error. WMS request not enabled. Check wms/ows_enable_request settings.' | ||
); | ||
}); | ||
}); | ||
}); | ||
@@ -94,2 +107,9 @@ | ||
title: 'Carte géologique image de la France au 1/50 000e', | ||
children: [ | ||
{ | ||
abstract: '', | ||
name: 'INHERIT_SCALE', | ||
title: 'Inherited scale denominators', | ||
}, | ||
], | ||
}, | ||
@@ -183,2 +203,14 @@ { | ||
title: 'GéoServices : géologie, hydrogéologie et gravimétrie', | ||
outputFormats: [ | ||
'image/png', | ||
'image/gif', | ||
'image/jpeg', | ||
'image/ecw', | ||
'image/tiff', | ||
'image/png; mode=8bit', | ||
'application/x-pdf', | ||
'image/svg+xml', | ||
], | ||
infoFormats: ['text/plain', 'application/vnd.ogc.gml'], | ||
exceptionFormats: ['XML', 'INIMAGE', 'BLANK'], | ||
keywords: [ | ||
@@ -193,2 +225,19 @@ 'Géologie', | ||
], | ||
provider: { | ||
contact: { | ||
name: 'Support BRGM', | ||
organization: 'BRGM', | ||
position: 'pointOfContact', | ||
phone: '+33(0)2 38 64 34 34', | ||
fax: '+33(0)2 38 64 35 18', | ||
address: { | ||
deliveryPoint: '3, Avenue Claude Guillemin, BP36009', | ||
city: 'Orléans', | ||
administrativeArea: 'Centre', | ||
postalCode: '45060', | ||
country: 'France', | ||
}, | ||
email: 'contact-brgm@brgm.fr', | ||
}, | ||
}, | ||
}); | ||
@@ -210,6 +259,41 @@ }); | ||
).toBe( | ||
'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0&LAYERS=layer1%2Clayer2&STYLES=&WIDTH=100&HEIGHT=200&FORMAT=image%2Fpng&CRS=EPSG%3A4326&BBOX=10%2C20%2C100%2C200' | ||
'http://geoservices.brgm.fr/geologie?language=fre&SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0&LAYERS=layer1%2Clayer2&STYLES=&WIDTH=100&HEIGHT=200&FORMAT=image%2Fpng&CRS=EPSG%3A4326&BBOX=10%2C20%2C100%2C200' | ||
); | ||
}); | ||
}); | ||
describe('#getCapabilitiesUrl', () => { | ||
it.skip('returns the URL used for the request before the capabilities are retrieved', async () => { | ||
expect(endpoint.getCapabilitiesUrl()).toBe( | ||
'https://my.test.service/ogc/wms?aa=bb&SERVICE=WMS&REQUEST=GetCapabilities' | ||
); | ||
await endpoint.isReady(); | ||
}); | ||
it('returns the self-reported URL after the capabilities are retrieved', async () => { | ||
await endpoint.isReady(); | ||
expect(endpoint.getCapabilitiesUrl()).toBe( | ||
'http://geoservices.brgm.fr/geologie?language=fre&SERVICE=WMS&REQUEST=GetCapabilities' | ||
); | ||
}); | ||
}); | ||
describe('#getOperationUrl', () => { | ||
it.skip('returns NULL before the document is loaded', async () => { | ||
expect(endpoint.getOperationUrl('GetMap')).toBeNull(); | ||
await endpoint.isReady(); | ||
}); | ||
it('returns undefined for a non-existant operation', async () => { | ||
await endpoint.isReady(); | ||
expect(endpoint.getOperationUrl('foo')).toBeUndefined(); | ||
}); | ||
it('returns the correct URL for an existant operation', async () => { | ||
await endpoint.isReady(); | ||
expect(endpoint.getOperationUrl('GetMap')).toBe( | ||
'http://geoservices.brgm.fr/geologie?language=fre&' | ||
); | ||
}); | ||
}); | ||
}); |
@@ -8,3 +8,6 @@ import { parseWmsCapabilities } from '../worker/index.js'; | ||
GenericEndpointInfo, | ||
type HttpMethod, | ||
MimeType, | ||
type OperationName, | ||
type OperationUrl, | ||
} from '../shared/models.js'; | ||
@@ -22,2 +25,3 @@ import { WmsLayerFull, WmsLayerSummary, WmsVersion } from './model.js'; | ||
private _layers: WmsLayerFull[] | null; | ||
private _url: Record<OperationName, OperationUrl>; | ||
private _version: WmsVersion | null; | ||
@@ -43,5 +47,6 @@ | ||
this._capabilitiesUrl | ||
).then(({ info, layers, version }) => { | ||
).then(({ info, layers, url, version }) => { | ||
this._info = info; | ||
this._layers = layers; | ||
this._url = url; | ||
this._version = version; | ||
@@ -165,3 +170,3 @@ }); | ||
return generateGetMapUrl( | ||
this._capabilitiesUrl, | ||
this.getOperationUrl('GetMap') || this._capabilitiesUrl, | ||
this._version, | ||
@@ -177,2 +182,31 @@ layers.join(','), | ||
} | ||
/** | ||
* Returns the Capabilities URL of the WMS | ||
* | ||
* This is the URL reported by the service if available, otherwise the URL | ||
* passed to the constructor | ||
*/ | ||
getCapabilitiesUrl() { | ||
const baseUrl = this.getOperationUrl('GetCapabilities'); | ||
if (!baseUrl) { | ||
return this._capabilitiesUrl; | ||
} | ||
return setQueryParams(baseUrl, { | ||
SERVICE: 'WMS', | ||
REQUEST: 'GetCapabilities', | ||
}); | ||
} | ||
/** | ||
* Returns the URL reported by the WMS for the given operation | ||
* @param operationName e.g. GetMap, GetCapabilities, etc. | ||
* @param method HTTP method | ||
*/ | ||
getOperationUrl(operationName: OperationName, method: HttpMethod = 'Get') { | ||
if (!this._url) { | ||
return null; | ||
} | ||
return this._url[operationName]?.[method]; | ||
} | ||
} |
@@ -1,2 +0,7 @@ | ||
import { BoundingBox, CrsCode, LayerStyle } from '../shared/models.js'; | ||
import { | ||
BoundingBox, | ||
CrsCode, | ||
LayerStyle, | ||
type MetadataURL, | ||
} from '../shared/models.js'; | ||
@@ -37,4 +42,7 @@ export type WmsLayerAttribution = { | ||
opaque: boolean; | ||
maxScaleDenominator?: number; | ||
minScaleDenominator?: number; | ||
attribution?: WmsLayerAttribution; | ||
keywords?: string[]; | ||
metadata?: MetadataURL[]; | ||
/** | ||
@@ -41,0 +49,0 @@ * Not defined if the layer is a leaf in the tree |
@@ -26,2 +26,20 @@ import { | ||
name: 'OGC WMTS', | ||
provider: { | ||
name: 'MiraMon', | ||
site: 'http://www.creaf.uab.cat/miramon', | ||
contact: { | ||
name: 'Joan Maso Pau', | ||
position: 'Senior Software Engineer', | ||
phone: '+34 93 581 1312', | ||
fax: '+34 93 581 4151', | ||
address: { | ||
deliveryPoint: 'Fac Ciencies UAB', | ||
city: 'Bellaterra', | ||
administrativeArea: 'Barcelona', | ||
postalCode: '08193', | ||
country: 'Spain', | ||
}, | ||
email: 'joan.maso@uab.cat', | ||
}, | ||
}, | ||
title: 'Web Map Tile Service', | ||
@@ -43,2 +61,20 @@ getTileUrls: { | ||
name: 'OGC WMTS', | ||
provider: { | ||
name: '', | ||
site: '', | ||
contact: { | ||
name: '', | ||
position: '', | ||
phone: '', | ||
fax: '', | ||
address: { | ||
deliveryPoint: '', | ||
city: '', | ||
administrativeArea: '', | ||
postalCode: '', | ||
country: '', | ||
}, | ||
email: '', | ||
}, | ||
}, | ||
title: 'Demographics_USA_Population_Density', | ||
@@ -81,2 +117,20 @@ getTileUrls: { | ||
name: 'OGC WMTS', | ||
provider: { | ||
name: 'IGN', | ||
site: '', | ||
contact: { | ||
name: 'Géoportail SAV', | ||
position: 'custodian', | ||
phone: '', | ||
fax: '', | ||
address: { | ||
deliveryPoint: '73 avenue de Paris', | ||
city: 'Saint Mandé', | ||
administrativeArea: '', | ||
postalCode: '94160', | ||
country: 'France', | ||
}, | ||
email: 'geop_services@geoportail.fr', | ||
}, | ||
}, | ||
title: 'Service de visualisation WMTS', | ||
@@ -83,0 +137,0 @@ getTileUrls: { |
import type { BoundingBox, LayerStyle } from '../shared/models.js'; | ||
import { readProviderFromCapabilities } from '../shared/ows.js'; | ||
import { | ||
@@ -62,2 +63,3 @@ findChildElement, | ||
keywords, | ||
provider: readProviderFromCapabilities(capabilitiesDoc), | ||
getTileUrls, | ||
@@ -64,0 +66,0 @@ }; |
@@ -243,2 +243,20 @@ import WmtsEndpoint from './endpoint.js'; | ||
name: 'OGC WMTS', | ||
provider: { | ||
name: 'MiraMon', | ||
site: 'http://www.creaf.uab.cat/miramon', | ||
contact: { | ||
name: 'Joan Maso Pau', | ||
position: 'Senior Software Engineer', | ||
phone: '+34 93 581 1312', | ||
fax: '+34 93 581 4151', | ||
address: { | ||
deliveryPoint: 'Fac Ciencies UAB', | ||
city: 'Bellaterra', | ||
administrativeArea: 'Barcelona', | ||
postalCode: '08193', | ||
country: 'Spain', | ||
}, | ||
email: 'joan.maso@uab.cat', | ||
}, | ||
}, | ||
title: 'Web Map Tile Service', | ||
@@ -349,2 +367,20 @@ getTileUrls: { | ||
title: 'Demographics_USA_Population_Density', | ||
provider: { | ||
name: '', | ||
site: '', | ||
contact: { | ||
name: '', | ||
position: '', | ||
phone: '', | ||
fax: '', | ||
address: { | ||
deliveryPoint: '', | ||
city: '', | ||
administrativeArea: '', | ||
postalCode: '', | ||
country: '', | ||
}, | ||
email: '', | ||
}, | ||
}, | ||
}); | ||
@@ -351,0 +387,0 @@ }); |
import { sendTaskRequest } from './utils.js'; | ||
import { GenericEndpointInfo } from '../shared/models.js'; | ||
import { | ||
GenericEndpointInfo, | ||
OperationName, | ||
OperationUrl, | ||
} from '../shared/models.js'; | ||
import { setFetchOptionsUpdateCallback } from '../shared/http-utils.js'; | ||
@@ -46,2 +50,3 @@ import { WmtsEndpointInfo, WmtsLayer, WmtsMatrixSet } from '../wmts/model.js'; | ||
version: WmsVersion; | ||
url: Record<OperationName, OperationUrl>; | ||
info: GenericEndpointInfo; | ||
@@ -61,2 +66,3 @@ layers: WmsLayerFull[]; | ||
version: WfsVersion; | ||
url: Record<OperationName, OperationUrl>; | ||
info: GenericEndpointInfo; | ||
@@ -63,0 +69,0 @@ featureTypes: WfsFeatureTypeInternal[]; |
import { addTaskHandler } from './utils.js'; | ||
import { queryXmlDocument, setFetchOptions } from '../shared/http-utils.js'; | ||
import { check } from '../shared/service-exception-error.js'; | ||
import * as wmsCapabilities from '../wms/capabilities.js'; | ||
@@ -15,15 +16,21 @@ import * as wfsCapabilities from '../wfs/capabilities.js'; | ||
addTaskHandler('parseWmsCapabilities', globalThis, ({ url }: { url: string }) => | ||
queryXmlDocument(url).then((xmlDoc) => ({ | ||
info: wmsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
layers: wmsCapabilities.readLayersFromCapabilities(xmlDoc), | ||
version: wmsCapabilities.readVersionFromCapabilities(xmlDoc), | ||
})) | ||
queryXmlDocument(url) | ||
.then((xmlDoc) => check(xmlDoc, url)) | ||
.then((xmlDoc) => ({ | ||
info: wmsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
layers: wmsCapabilities.readLayersFromCapabilities(xmlDoc), | ||
url: wmsCapabilities.readOperationUrlsFromCapabilities(xmlDoc), | ||
version: wmsCapabilities.readVersionFromCapabilities(xmlDoc), | ||
})) | ||
); | ||
addTaskHandler('parseWfsCapabilities', globalThis, ({ url }: { url: string }) => | ||
queryXmlDocument(url).then((xmlDoc) => ({ | ||
info: wfsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
featureTypes: wfsCapabilities.readFeatureTypesFromCapabilities(xmlDoc), | ||
version: wfsCapabilities.readVersionFromCapabilities(xmlDoc), | ||
})) | ||
queryXmlDocument(url) | ||
.then((xmlDoc) => check(xmlDoc, url)) | ||
.then((xmlDoc) => ({ | ||
info: wfsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
featureTypes: wfsCapabilities.readFeatureTypesFromCapabilities(xmlDoc), | ||
url: wfsCapabilities.readOperationUrlsFromCapabilities(xmlDoc), | ||
version: wfsCapabilities.readVersionFromCapabilities(xmlDoc), | ||
})) | ||
); | ||
@@ -72,7 +79,9 @@ | ||
({ url }: { url: string }) => | ||
queryXmlDocument(url).then((xmlDoc) => ({ | ||
info: wmtsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
layers: wmtsCapabilities.readLayersFromCapabilities(xmlDoc), | ||
matrixSets: wmtsCapabilities.readMatrixSetsFromCapabilities(xmlDoc), | ||
})) | ||
queryXmlDocument(url) | ||
.then((xmlDoc) => check(xmlDoc, url)) | ||
.then((xmlDoc) => ({ | ||
info: wmtsCapabilities.readInfoFromCapabilities(xmlDoc), | ||
layers: wmtsCapabilities.readLayersFromCapabilities(xmlDoc), | ||
matrixSets: wmtsCapabilities.readMatrixSetsFromCapabilities(xmlDoc), | ||
})) | ||
); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1446041
204
23852