import { Bounds } from './bounds.js';
import { Epsg } from './epsg.js';
import { getXyOrder } from './xy.order.js';
/** order by increasing zoom level */
function compareMatrix(a, b) {
    return b.scaleDenominator - a.scaleDenominator;
}
export class TileMatrixSet {
    /**
       * Create using a WMTS EPSG definition
  
       * @param def
       */
    constructor(def) {
        /** Projection of the matrix set */
        Object.defineProperty(this, "projection", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Number of pixels for a tile */
        Object.defineProperty(this, "tileHeight", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "tileWidth", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /**
         * Raw tile matrix definition
         *
         * Avoid directly accessing this object
         */
        Object.defineProperty(this, "def", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Indexed tile index zooms */
        Object.defineProperty(this, "zooms", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: []
        });
        /** Array index of X coordinates */
        Object.defineProperty(this, "indexX", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
        /** Array index of y coordinate */
        Object.defineProperty(this, "indexY", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 1
        });
        /** The full extent (boundingBox) from the TileMatrixSetType definition */
        Object.defineProperty(this, "extent", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        /** Mapping of scaleFactor to closet scaleFactor  */
        Object.defineProperty(this, "zoomConversionMap", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: new Map()
        });
        this.def = def;
        // this.tileSize = def.tileMatrix[0].tileHeight;
        this.tileWidth = def.tileMatrix[0].tileWidth;
        this.tileHeight = def.tileMatrix[0].tileHeight;
        const zooms = def.tileMatrix.slice().sort(compareMatrix);
        const dups = {};
        for (const z of zooms) {
            if (dups[z.identifier])
                throw new Error(`Duplicate tileMatrix identifier ${z.identifier}`);
            if (z.tileHeight !== this.tileHeight)
                throw new Error('All tiles must have the same tile size');
            if (z.tileWidth !== this.tileWidth)
                throw new Error('All tiles must have the same tile size');
            dups[z.identifier] = true;
        }
        this.zooms = zooms;
        const projection = Epsg.parse(def.supportedCRS);
        if (projection == null)
            throw new Error(`Unable to find supported projection ${def.supportedCRS}`);
        this.projection = projection;
        /** Some projections @see EPSG:2193 are defined with XY Order of  */
        if (getXyOrder(this.projection) === 'yx') {
            this.indexX = 1;
            this.indexY = 0;
        }
        const { lowerCorner, upperCorner } = def.boundingBox;
        const x = lowerCorner[this.indexX];
        const y = lowerCorner[this.indexY];
        this.extent = new Bounds(x, y, upperCorner[this.indexX] - x, upperCorner[this.indexY] - y);
    }
    /**
     * Maximum zoom level
     * Valid zoom levels are 0 - maxZoom (inclusive)
     */
    get maxZoom() {
        return this.zooms.length - 1;
    }
    /**
     *  Unique identifier of the TileMatrix {@link TileMatrixSetType.identifier}
     *
     * @example
     * "WebMercatorQuad"
     * "NZTM2000Quad"
     */
    get identifier() {
        return this.def.identifier;
    }
    /** Get the pixels / meter at a specified zoom level */
    pixelScale(zoom) {
        const z = this.zooms[zoom];
        if (z == null)
            throw new Error(`Zoom not found :${zoom}`);
        // TODO support non meters projections
        /**
         * http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2:
         * The pixel size of the tile can be obtained from the scaleDenominator by
         * multiplying the later by 0.28 10-3 / metersPerUnit
         */
        return z.scaleDenominator * 0.28e-3;
    }
    tileToPixels(tX, tY) {
        return { x: tX * this.tileWidth, y: tY * this.tileHeight };
    }
    /**
     * Convert a pixel point into tile offset
     *
     * @param pX pixel X offset
     * @param pY pixel Y offset
     * @param zoom pixel zoom level
     */
    pixelsToTile(pX, pY, zoom) {
        const x = Math.floor(pX / this.tileWidth);
        const y = Math.floor(pY / this.tileHeight);
        return { x, y, z: zoom };
    }
    /**
     * Convert a XYZ tile into the raster bounds for the tile
     *
     * @param tX tile X offset
     * @param tY tile Y offset
     * @param zoom tile zoom level
     */
    sourceToPixels(sX, sY, zoom) {
        const z = this.zooms[zoom];
        const scale = this.pixelScale(zoom);
        const pX = (sX - z.topLeftCorner[this.indexX]) / scale;
        const pY = (z.topLeftCorner[this.indexY] - sY) / scale;
        return { x: pX, y: pY };
    }
    /**
     * Convert a pixel offset into source units ()
     * @param pX pixel X offset
     * @param pY pixel Y offset
     * @param zoom pixel zoom level
     */
    pixelsToSource(pX, pY, zoom) {
        const z = this.zooms[zoom];
        const scale = this.pixelScale(zoom);
        const sX = z.topLeftCorner[this.indexX] + pX * scale;
        const sY = z.topLeftCorner[this.indexY] - pY * scale;
        return { x: sX, y: sY };
    }
    /**
     * Get the source units for a `tile` upper left point
     */
    tileToSource(tile) {
        const ul = this.tileToPixels(tile.x, tile.y);
        return this.pixelsToSource(ul.x, ul.y, tile.z);
    }
    /**
     * Get the boundingBox for a `tile` in source units
     */
    tileToSourceBounds(tile) {
        const ul = this.tileToSource(tile);
        const scale = this.pixelScale(tile.z);
        const width = scale * this.tileWidth;
        const height = scale * this.tileHeight;
        return new Bounds(ul.x, ul.y - height, width, height);
    }
    /**
     * Iterate over the top level tiles that cover the extent of the `TileMatrixSet`
     */
    *topLevelTiles() {
        const z = 0;
        const { matrixWidth, matrixHeight } = this.zooms[0];
        for (let y = 0; y < matrixHeight; ++y) {
            for (let x = 0; x < matrixWidth; ++x) {
                yield { x, y, z };
            }
        }
        return null;
    }
    /**
       * Iterate over the child (`z+1`) tiles that cover `tile`
  
       * @param tile
       */
    *coverTile(tile) {
        if (tile == null) {
            yield* this.topLevelTiles();
            return null;
        }
        const z = tile.z + 1;
        const { matrixWidth, matrixHeight } = this.zooms[tile.z];
        const { matrixWidth: childWidth, matrixHeight: childHeight } = this.zooms[z];
        const xScale = childWidth / matrixWidth;
        const yScale = childHeight / matrixHeight;
        const xStart = Math.floor(tile.x * xScale);
        const yStart = Math.floor(tile.y * yScale);
        const xEnd = Math.ceil((tile.x + 1) * xScale);
        const yEnd = Math.ceil((tile.y + 1) * yScale);
        for (let y = yStart; y < yEnd; ++y) {
            for (let x = xStart; x < xEnd; ++x) {
                yield { x, y, z };
            }
        }
        return null;
    }
    /**
     * Make a name for a `tile`
     */
    static tileToName(tile) {
        return `${tile.z}-${tile.x}-${tile.y}`;
    }
    /**
     * Convert `name` to a `tile`
     */
    static nameToTile(name) {
        const parts = name.split('-');
        if (parts.length === 3) {
            const z = Number(parts[0]);
            const x = Number(parts[1]);
            const y = Number(parts[2]);
            if (!(isNaN(z) || isNaN(y) || isNaN(x))) {
                return { x, y, z };
            }
        }
        throw new Error(`Invalid tile name '${name}'`);
    }
    /**
     * Find the closest matching zoom to the given scale
     * @param scaleDenominator scale to match
     * @returns the zoom level of the closest matching zoom
     */
    findBestZoom(scaleDenominator) {
        let zoom = this.zoomConversionMap.get(scaleDenominator);
        if (zoom == null) {
            zoom = this.convertZoomLevel(scaleDenominator);
            this.zoomConversionMap.set(scaleDenominator, zoom);
        }
        return zoom;
    }
    convertZoomLevel(scaleDenominator) {
        for (let i = 0; i < this.zooms.length; i++) {
            const scaleDiff = this.zooms[i].scaleDenominator - scaleDenominator;
            if (scaleDiff > 0.00001)
                continue;
            if (i === 0)
                return i;
            // Check the previous scale diff as it may actually be closer in scale
            const lastScaleDiff = this.zooms[i - 1].scaleDenominator - scaleDenominator;
            const isCurrentSmaller = Math.abs(scaleDiff) < lastScaleDiff;
            if (isCurrentSmaller)
                return i;
            return i - 1;
        }
        return this.maxZoom;
    }
    /**
     * Convert a given zoom level from one tile matrix to another
     * @param z Zoom to convert
     * @param from Tile matrix to convert from
     * @param to  Tile matrix to convert to
     * @param mapMinMax Should the min (0) and max be converted directly across,
     *  for example if from.maxZoom = 10 and to.maxZoom = 30, when z is 10 it will always return 30, even if there are other scales that would match
     * @returns converted zoom level
     */
    static convertZoomLevel(z, from, to, mapMinMax = true) {
        // Same matrix no mapping needed
        if (from.identifier === to.identifier)
            return z;
        if (z >= from.maxZoom)
            z = from.maxZoom;
        // Map min/max to min/max zoom
        if (mapMinMax && z === 0)
            return z;
        if (mapMinMax && z === from.maxZoom)
            return to.maxZoom;
        return to.findBestZoom(from.zooms[z].scaleDenominator);
    }
}
/**
 * http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2:
 * The pixel size of the tile can be obtained from the scaleDenominator by
 * multiplying the later by 0.28 10-3 / metersPerUnit
 */
Object.defineProperty(TileMatrixSet, "ScaleDenominatorRatio", {
    enumerable: true,
    configurable: true,
    writable: true,
    value: 0.28e-3
});
//# sourceMappingURL=tile.matrix.set.js.map