"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tile_source_1 = require("./tile_source");
const p = require("../../core/properties");
const array_1 = require("../../core/util/array");
const tile_utils_1 = require("./tile_utils");
class MercatorTileSource extends tile_source_1.TileSource {
    constructor(attrs) {
        super(attrs);
    }
    static init_MercatorTileSource() {
        this.define({
            snap_to_zoom: [p.Boolean, false],
            wrap_around: [p.Boolean, true],
        });
        this.override({
            x_origin_offset: 20037508.34,
            y_origin_offset: 20037508.34,
            initial_resolution: 156543.03392804097,
        });
    }
    initialize() {
        super.initialize();
        this._resolutions = array_1.range(this.min_zoom, this.max_zoom + 1).map((z) => this.get_resolution(z));
    }
    _computed_initial_resolution() {
        if (this.initial_resolution != null)
            return this.initial_resolution;
        else {
            // TODO testing 2015-11-17, if this codepath is used it seems
            // to use 100% cpu and wedge Chrome
            return (2 * Math.PI * 6378137) / this.tile_size;
        }
    }
    is_valid_tile(x, y, z) {
        if (!this.wrap_around) {
            if (x < 0 || x >= Math.pow(2, z))
                return false;
        }
        if (y < 0 || y >= Math.pow(2, z))
            return false;
        return true;
    }
    parent_by_tile_xyz(x, y, z) {
        const quadkey = this.tile_xyz_to_quadkey(x, y, z);
        const parent_quadkey = quadkey.substring(0, quadkey.length - 1);
        return this.quadkey_to_tile_xyz(parent_quadkey);
    }
    get_resolution(level) {
        return this._computed_initial_resolution() / Math.pow(2, level);
    }
    get_resolution_by_extent(extent, height, width) {
        const x_rs = (extent[2] - extent[0]) / width;
        const y_rs = (extent[3] - extent[1]) / height;
        return [x_rs, y_rs];
    }
    get_level_by_extent(extent, height, width) {
        const x_rs = (extent[2] - extent[0]) / width;
        const y_rs = (extent[3] - extent[1]) / height;
        const resolution = Math.max(x_rs, y_rs);
        let i = 0;
        for (const r of this._resolutions) {
            if (resolution > r) {
                if (i == 0)
                    return 0;
                if (i > 0)
                    return i - 1;
            }
            i += 1;
        }
        // otherwise return the highest available resolution
        return (i - 1);
    }
    get_closest_level_by_extent(extent, height, width) {
        const x_rs = (extent[2] - extent[0]) / width;
        const y_rs = (extent[3] - extent[1]) / height;
        const resolution = Math.max(x_rs, y_rs);
        const closest = this._resolutions.reduce(function (previous, current) {
            if (Math.abs(current - resolution) < Math.abs(previous - resolution))
                return current;
            else
                return previous;
        });
        return this._resolutions.indexOf(closest);
    }
    snap_to_zoom_level(extent, height, width, level) {
        const [xmin, ymin, xmax, ymax] = extent;
        const desired_res = this._resolutions[level];
        let desired_x_delta = width * desired_res;
        let desired_y_delta = height * desired_res;
        if (!this.snap_to_zoom) {
            const xscale = (xmax - xmin) / desired_x_delta;
            const yscale = (ymax - ymin) / desired_y_delta;
            if (xscale > yscale) {
                desired_x_delta = (xmax - xmin);
                desired_y_delta = desired_y_delta * xscale;
            }
            else {
                desired_x_delta = desired_x_delta * yscale;
                desired_y_delta = (ymax - ymin);
            }
        }
        const x_adjust = (desired_x_delta - (xmax - xmin)) / 2;
        const y_adjust = (desired_y_delta - (ymax - ymin)) / 2;
        return [xmin - x_adjust, ymin - y_adjust, xmax + x_adjust, ymax + y_adjust];
    }
    tms_to_wmts(x, y, z) {
        'Note this works both ways';
        return [x, Math.pow(2, z) - 1 - y, z];
    }
    wmts_to_tms(x, y, z) {
        'Note this works both ways';
        return [x, Math.pow(2, z) - 1 - y, z];
    }
    pixels_to_meters(px, py, level) {
        const res = this.get_resolution(level);
        const mx = (px * res) - this.x_origin_offset;
        const my = (py * res) - this.y_origin_offset;
        return [mx, my];
    }
    meters_to_pixels(mx, my, level) {
        const res = this.get_resolution(level);
        const px = (mx + this.x_origin_offset) / res;
        const py = (my + this.y_origin_offset) / res;
        return [px, py];
    }
    pixels_to_tile(px, py) {
        let tx = Math.ceil(px / this.tile_size);
        tx = tx === 0 ? tx : tx - 1;
        const ty = Math.max(Math.ceil(py / this.tile_size) - 1, 0);
        return [tx, ty];
    }
    pixels_to_raster(px, py, level) {
        const mapSize = this.tile_size << level;
        return [px, mapSize - py];
    }
    meters_to_tile(mx, my, level) {
        const [px, py] = this.meters_to_pixels(mx, my, level);
        return this.pixels_to_tile(px, py);
    }
    get_tile_meter_bounds(tx, ty, level) {
        // expects tms styles coordinates (bottom-left origin)
        const [xmin, ymin] = this.pixels_to_meters(tx * this.tile_size, ty * this.tile_size, level);
        const [xmax, ymax] = this.pixels_to_meters((tx + 1) * this.tile_size, (ty + 1) * this.tile_size, level);
        return [xmin, ymin, xmax, ymax];
    }
    get_tile_geographic_bounds(tx, ty, level) {
        const bounds = this.get_tile_meter_bounds(tx, ty, level);
        const [minLon, minLat, maxLon, maxLat] = tile_utils_1.meters_extent_to_geographic(bounds);
        return [minLon, minLat, maxLon, maxLat];
    }
    get_tiles_by_extent(extent, level, tile_border = 1) {
        // unpack extent and convert to tile coordinates
        const [xmin, ymin, xmax, ymax] = extent;
        let [txmin, tymin] = this.meters_to_tile(xmin, ymin, level);
        let [txmax, tymax] = this.meters_to_tile(xmax, ymax, level);
        // add tiles which border
        txmin -= tile_border;
        tymin -= tile_border;
        txmax += tile_border;
        tymax += tile_border;
        const tiles = [];
        for (let ty = tymax; ty >= tymin; ty--) {
            for (let tx = txmin; tx <= txmax; tx++) {
                if (this.is_valid_tile(tx, ty, level))
                    tiles.push([tx, ty, level, this.get_tile_meter_bounds(tx, ty, level)]);
            }
        }
        this.sort_tiles_from_center(tiles, [txmin, tymin, txmax, tymax]);
        return tiles;
    }
    quadkey_to_tile_xyz(quadKey) {
        /**
         * Computes tile x, y and z values based on quadKey.
         */
        let tileX = 0;
        let tileY = 0;
        const tileZ = quadKey.length;
        for (let i = tileZ; i > 0; i--) {
            const value = quadKey.charAt(tileZ - i);
            const mask = 1 << (i - 1);
            switch (value) {
                case '0':
                    continue;
                case '1':
                    tileX |= mask;
                    break;
                case '2':
                    tileY |= mask;
                    break;
                case '3':
                    tileX |= mask;
                    tileY |= mask;
                    break;
                default:
                    throw new TypeError(`Invalid Quadkey: ${quadKey}`);
            }
        }
        return [tileX, tileY, tileZ];
    }
    tile_xyz_to_quadkey(x, y, z) {
        /*
         * Computes quadkey value based on tile x, y and z values.
         */
        let quadkey = "";
        for (let i = z; i > 0; i--) {
            const mask = 1 << (i - 1);
            let digit = 0;
            if ((x & mask) !== 0) {
                digit += 1;
            }
            if ((y & mask) !== 0) {
                digit += 2;
            }
            quadkey += digit.toString();
        }
        return quadkey;
    }
    children_by_tile_xyz(x, y, z) {
        const quadkey = this.tile_xyz_to_quadkey(x, y, z);
        const child_tile_xyz = [];
        for (let i = 0; i <= 3; i++) {
            const [x, y, z] = this.quadkey_to_tile_xyz(quadkey + i.toString());
            const b = this.get_tile_meter_bounds(x, y, z);
            child_tile_xyz.push([x, y, z, b]);
        }
        return child_tile_xyz;
    }
    get_closest_parent_by_tile_xyz(x, y, z) {
        const world_x = this.calculate_world_x_by_tile_xyz(x, y, z);
        [x, y, z] = this.normalize_xyz(x, y, z);
        let quadkey = this.tile_xyz_to_quadkey(x, y, z);
        while (quadkey.length > 0) {
            quadkey = quadkey.substring(0, quadkey.length - 1);
            [x, y, z] = this.quadkey_to_tile_xyz(quadkey);
            [x, y, z] = this.denormalize_xyz(x, y, z, world_x);
            if (this.tiles.has(this.tile_xyz_to_key(x, y, z)))
                return [x, y, z];
        }
        return [0, 0, 0];
    }
    normalize_xyz(x, y, z) {
        if (this.wrap_around) {
            const tile_count = Math.pow(2, z);
            return [((x % tile_count) + tile_count) % tile_count, y, z];
        }
        else {
            return [x, y, z];
        }
    }
    denormalize_xyz(x, y, z, world_x) {
        return [x + (world_x * Math.pow(2, z)), y, z];
    }
    denormalize_meters(meters_x, meters_y, _level, world_x) {
        return [meters_x + (world_x * 2 * Math.PI * 6378137), meters_y];
    }
    calculate_world_x_by_tile_xyz(x, _y, z) {
        return Math.floor(x / Math.pow(2, z));
    }
}
exports.MercatorTileSource = MercatorTileSource;
MercatorTileSource.__name__ = "MercatorTileSource";
MercatorTileSource.init_MercatorTileSource();
