import { standardizeLayerName } from '@basemaps/config';
import { Bounds, GoogleTms, Projection } from '@basemaps/geo';
import { toQueryString, V } from '@basemaps/shared';
import { ImageFormatOrder } from '@basemaps/tiler';
const CapabilitiesAttrs = {
    xmlns: 'http://www.opengis.net/wmts/1.0',
    'xmlns:ows': 'http://www.opengis.net/ows/1.1',
    'xmlns:xlink': 'http://www.w3.org/1999/xlink',
    'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    'xmlns:gml': 'http://www.opengis.net/gml',
    'xsi:schemaLocation': 'http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd',
    version: '1.0.0',
};
function wgs84Extent(tileMatrix, bbox) {
    return Projection.get(tileMatrix).boundsToWgs84BoundingBox(bbox);
}
/** Number of decimal places to use in lat lng */
const LngLatPrecision = 6;
const MeterPrecision = 4;
function formatCoords(x, precision) {
    return Number(x.toFixed(precision)).toString();
}
/** Format a bounding box XY as `${x} ${y}` while restricting to precision decimal places */
function formatBbox(x, y, precision) {
    return `${formatCoords(x, precision)} ${formatCoords(y, precision)}`;
}
export class WmtsBuilder {
    constructor(params) {
        Object.defineProperty(this, "httpBase", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "apiKey", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "config", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "pipeline", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "filters", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** All the imagery used by the tileSet and tileMatrixes */
        Object.defineProperty(this, "imagery", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: new Map()
        });
        Object.defineProperty(this, "formats", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: []
        });
        Object.defineProperty(this, "tileMatrixSets", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: new Map()
        });
        this.httpBase = params.httpBase;
        this.apiKey = params.apiKey;
        this.config = params.config;
        this.pipeline = params.pipeline;
    }
    addImagery(...imagery) {
        for (const im of imagery)
            this.imagery.set(im.id, im);
    }
    addTileMatrix(...tileMatrix) {
        for (const tms of tileMatrix)
            this.tileMatrixSets.set(tms.identifier, tms);
    }
    addFormats(...formats) {
        for (const format of formats)
            this.formats.push(format);
    }
    getFormats() {
        if (this.formats.length)
            return this.formats;
        return ImageFormatOrder;
    }
    getMatrixSets(tileSet) {
        const matrixSets = new Set();
        for (const tms of this.tileMatrixSets.values()) {
            if (tileSet.layers.find((f) => f[tms.projection.code] != null)) {
                matrixSets.add(tms);
            }
        }
        return matrixSets;
    }
    buildKeywords(tileSet) {
        if (tileSet.category == null)
            return V('ows:Keywords');
        return V('ows:Keywords', [V('ows:Keyword', tileSet.category)]);
    }
    buildWgs84BoundingBox(tms, layers) {
        let bbox = null;
        if (layers.length > 0) {
            let bounds = layers[0];
            for (let i = 1; i < layers.length; i++) {
                bounds = bounds.union(layers[i]);
            }
            // If imagery is outside of the bounds of the tileMatrix, fall back to the tileMatrix extent for the bounds
            if (!tms.extent.containsBounds(bounds)) {
                bbox = wgs84Extent(tms, tms.extent);
            }
            else {
                bbox = wgs84Extent(tms, bounds);
            }
        }
        // No layers provided assume extent is the size of the tile matrix set :shrug: ?
        if (bbox == null)
            bbox = wgs84Extent(tms, tms.extent);
        // If east is less than west, then this has crossed the anti meridian, so cover the entire globe
        if (bbox[2] < bbox[0]) {
            bbox[0] = -180;
            bbox[2] = 180;
        }
        return V('ows:WGS84BoundingBox', { crs: 'urn:ogc:def:crs:OGC:2:84' }, [
            V('ows:LowerCorner', formatBbox(bbox[0], bbox[1], LngLatPrecision)),
            V('ows:UpperCorner', formatBbox(bbox[2], bbox[3], LngLatPrecision)),
        ]);
    }
    /** Combine all the bounds of the imagery inside the layers into a extent for the imagery set */
    buildBoundingBoxFromImagery(tms, layers) {
        let bounds = null;
        for (const layer of layers) {
            const imgId = layer[tms.projection.code];
            if (imgId == null)
                continue;
            const img = this.imagery.get(imgId);
            if (img == null)
                continue;
            if (bounds == null)
                bounds = Bounds.fromJson(img.bounds);
            else
                bounds = bounds.union(Bounds.fromJson(img.bounds));
        }
        if (bounds == null)
            return null;
        // If imagery is outside of the bounds of the tileMatrix, fall back to the tileMatrix extent for the bounds
        if (!tms.extent.containsBounds(bounds))
            bounds = tms.extent;
        const bbox = bounds.toBbox();
        return V('ows:BoundingBox', { crs: tms.projection.toUrn() }, [
            V('ows:LowerCorner', formatBbox(bbox[tms.indexX], bbox[tms.indexY], MeterPrecision)),
            V('ows:UpperCorner', formatBbox(bbox[tms.indexX + 2], bbox[tms.indexY + 2], MeterPrecision)),
        ]);
    }
    buildStyle() {
        return V('Style', { isDefault: 'true' }, [V('ows:Title', 'Default Style'), V('ows:Identifier', 'default')]);
    }
    buildResourceUrl(tileSetId, suffix) {
        return V('ResourceURL', {
            format: 'image/' + suffix,
            resourceType: 'tile',
            template: this.buildTileUrl(tileSetId, suffix),
        });
    }
    buildTileUrl(tileSetId, suffix) {
        // TODO this should restrict the output formats to supported formats in pipelines
        const query = { api: this.apiKey, config: this.config, pipeline: this.pipeline };
        return [
            this.httpBase,
            'v1',
            'tiles',
            tileSetId,
            '{TileMatrixSet}',
            '{TileMatrix}',
            '{TileCol}',
            `{TileRow}.${suffix}${toQueryString(query)}`,
        ].join('/');
    }
    buildFormats() {
        return this.getFormats().map((fmt) => V('Format', 'image/' + fmt));
    }
    buildTileMatrixLink(tileSet) {
        const matrixSetNodes = [];
        for (const tms of this.tileMatrixSets.values()) {
            if (tileSet.layers.find((f) => f[tms.projection.code] != null)) {
                matrixSetNodes.push(V('TileMatrixSetLink', [V('TileMatrixSet', tms.identifier)]));
            }
        }
        return matrixSetNodes;
    }
    buildLayerFromImagery(layer) {
        const matrixSets = new Set();
        const matrixSetNodes = [];
        for (const tms of this.tileMatrixSets.values()) {
            const imdIg = layer[tms.projection.code];
            if (imdIg == null)
                continue;
            const img = this.imagery.get(imdIg);
            if (img == null)
                continue;
            matrixSetNodes.push(V('TileMatrixSetLink', [V('TileMatrixSet', tms.identifier)]));
            matrixSets.add(tms);
        }
        const layerNameId = standardizeLayerName(layer.name);
        const matrixSetList = [...matrixSets.values()];
        const firstMatrix = matrixSetList[0];
        if (firstMatrix == null)
            return null;
        const firstImg = this.imagery.get(layer[firstMatrix.projection.code] ?? '');
        if (firstImg == null)
            return null;
        return V('Layer', [
            V('ows:Title', layer.title),
            V('ows:Abstract', ''),
            V('ows:Identifier', layerNameId),
            this.buildKeywords(firstImg),
            ...matrixSetList.map((tms) => {
                return this.buildBoundingBoxFromImagery(tms, [layer]);
            }),
            this.buildWgs84BoundingBox(firstMatrix, [Bounds.fromJson(firstImg.bounds)]),
            this.buildStyle(),
            ...this.getFormats().map((fmt) => V('Format', 'image/' + fmt)),
            ...matrixSetNodes,
            ...this.getFormats().map((fmt) => this.buildResourceUrl(layerNameId, fmt)),
        ]);
    }
}
/**
 * WMTS Capabilities Builder
 *
 * /v1/tiles/:tileSet/:tileMatrix/WMTSCapabilities.xml
 * /v1/tiles/:tileSet/WMTSCapabilities.xml
 * /v1/tiles/WMTSCapabilities.xml
 *
 * @example
 *
 */
export class WmtsCapabilities extends WmtsBuilder {
    constructor(params) {
        super(params);
        Object.defineProperty(this, "minZoom", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
        Object.defineProperty(this, "maxZoom", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 32
        });
        /** Wmts tileSet layer and imagery layers information */
        Object.defineProperty(this, "tileSet", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "configLayers", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Wmts Provider information */
        Object.defineProperty(this, "provider", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
    }
    addTileSet(tileSet) {
        this.tileSet = tileSet;
    }
    addLayers(configLayers) {
        this.configLayers = configLayers;
    }
    addProvider(provider) {
        this.provider = provider;
    }
    addPipeline(pipeline) {
        this.pipeline = pipeline;
    }
    toProviderVNode(provider) {
        if (provider == null)
            return [];
        const { serviceIdentification, serviceProvider } = provider;
        const { contact } = serviceProvider;
        return [
            V('ows:ServiceIdentification', [
                V('ows:Title', serviceIdentification.title),
                V('ows:Abstract', serviceIdentification.description),
                V('ows:ServiceType', 'OGC WMTS'),
                V('ows:ServiceTypeVersion', '1.0.0'),
                V('ows:Fees', serviceIdentification.fees),
                V('ows:AccessConstraints', serviceIdentification.accessConstraints),
            ]),
            V('ows:ServiceProvider', [
                V('ows:ProviderName', serviceProvider.name),
                V('ows:ProviderSite', { 'xlink:href': serviceProvider.site }),
                V('ows:ServiceContact', [
                    V('ows:IndividualName', contact.individualName),
                    V('ows:PositionName', contact.position),
                    V('ows:ContactInfo', [
                        V('ows:Phone', [V('ows:Voice', contact.phone)]),
                        V('ows:Address', [
                            V('ows:DeliveryPoint', contact.address.deliveryPoint),
                            V('ows:City', contact.address.city),
                            V('ows:PostalCode', contact.address.postalCode),
                            V('ows:Country', contact.address.country),
                            V('ows:ElectronicMailAddress', contact.address.email),
                        ]),
                    ]),
                ]),
            ]),
        ];
    }
    toLayerVNode(tileSet) {
        const matrixSets = this.getMatrixSets(tileSet);
        const matrixSetList = [...matrixSets.values()];
        const firstMatrix = matrixSetList[0];
        if (firstMatrix == null)
            throw new Error('No matrix sets found for layer ' + tileSet.name);
        // Prefer using the web mercator tms for bounds
        const webMercatorOrFirst = matrixSetList.find((f) => f.identifier === GoogleTms.identifier) ?? firstMatrix;
        const bounds = [];
        for (const l of tileSet.layers) {
            const img = this.imagery.get(l[webMercatorOrFirst.projection.code] ?? '');
            if (img == null)
                continue;
            bounds.push(Bounds.fromJson(img.bounds));
        }
        const layerNameId = standardizeLayerName(tileSet.name);
        return V('Layer', [
            V('ows:Title', tileSet.title),
            V('ows:Abstract', tileSet.description ?? ''),
            V('ows:Identifier', layerNameId),
            this.buildKeywords(tileSet),
            ...[...matrixSets.values()].map((tms) => this.buildBoundingBoxFromImagery(tms, tileSet.layers)),
            this.buildWgs84BoundingBox(webMercatorOrFirst, bounds),
            this.buildStyle(),
            ...this.buildFormats(),
            ...this.buildTileMatrixLink(tileSet),
            ...this.getFormats().map((fmt) => this.buildResourceUrl(layerNameId, fmt)),
        ]);
    }
    toAllImageryLayersVNode(configLayers) {
        if (configLayers == null)
            return [];
        const layersVNode = [];
        const layerByName = new Map();
        // Dedupe the layers by unique name
        for (const img of configLayers)
            layerByName.set(standardizeLayerName(img.name), img);
        const orderedLayers = Array.from(layerByName.values()).sort((a, b) => (a.title ?? a.name).localeCompare(b.title ?? b.name));
        for (const img of orderedLayers) {
            const layer = this.buildLayerFromImagery(img);
            if (layer)
                layersVNode.push(layer);
        }
        return layersVNode;
    }
    buildTileMatrixSet(tms) {
        return V('TileMatrixSet', [
            V('ows:Title', tms.def.title),
            tms.def.abstract ? V('ows:Abstract', tms.def.abstract) : null,
            V('ows:Identifier', tms.identifier),
            V('ows:SupportedCRS', tms.projection.toUrn()),
            tms.def.wellKnownScaleSet ? V('WellKnownScaleSet', tms.def.wellKnownScaleSet) : null,
            ...tms.def.tileMatrix.slice(this.minZoom, this.maxZoom + 1).map((c) => {
                return V('TileMatrix', [
                    V('ows:Identifier', c.identifier),
                    V('ScaleDenominator', c.scaleDenominator),
                    V('TopLeftCorner', c.topLeftCorner.join(' ')),
                    V('TileWidth', c.tileWidth),
                    V('TileHeight', c.tileHeight),
                    V('MatrixWidth', c.matrixWidth),
                    V('MatrixHeight', c.matrixHeight),
                ]);
            }),
        ]);
    }
    toVNode() {
        if (this.tileSet == null)
            throw new Error('No Tileset provided');
        // Prepare provider vNode if exists
        const provider = this.toProviderVNode(this.provider);
        // Build TileSet Layer VNodes
        const layers = [];
        layers.push(this.toLayerVNode(this.tileSet));
        const contents = layers.concat(this.toAllImageryLayersVNode(this.configLayers));
        // Build TileMatrix Sets vNodes
        for (const tms of this.tileMatrixSets.values())
            contents.push(this.buildTileMatrixSet(tms));
        return V('Capabilities', CapabilitiesAttrs, [...provider, V('Contents', contents)]);
    }
    toXml() {
        return '<?xml version="1.0" encoding="utf-8"?>\n' + this.toVNode().toString();
    }
    fromParams(params) {
        for (const tileMatrix of params.tileMatrix)
            this.addTileMatrix(tileMatrix);
        for (const im of params.imagery.values())
            this.addImagery(im);
        for (const format of params.formats)
            this.addFormats(format);
        // Build wmts capabilities
        this.addTileSet(params.tileSet);
        this.addLayers(params.layers);
        this.addProvider(params.provider);
        if (params.pipeline)
            this.addPipeline(params.pipeline);
    }
}
//# sourceMappingURL=wmts.capability.js.map