UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

285 lines 10.4 kB
import { TileSource } from "./tile_source"; import { range } from "../../core/util/array"; import { meters_extent_to_geographic } from "./tile_utils"; export class MercatorTileSource extends TileSource { static __name__ = "MercatorTileSource"; constructor(attrs) { super(attrs); } static { this.define(({ Bool }) => ({ snap_to_zoom: [Bool, false], wrap_around: [Bool, true], })); this.override({ x_origin_offset: 20037508.34, y_origin_offset: 20037508.34, initial_resolution: 156543.03392804097, }); } _resolutions; initialize() { super.initialize(); this._resolutions = 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 >= 2 ** z) { return false; } } if (y < 0 || y >= 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() / 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]; } rescale(extent, height, width, last_height, last_width) { const [xmin, ymin, xmax, ymax] = extent; const x_delta = xmax - xmin; const y_delta = ymax - ymin; const x_scale = width / last_width; const y_scale = height / last_height; const desired_x_delta = x_delta * x_scale; const desired_y_delta = y_delta * y_scale; const x_adjust = desired_x_delta - x_delta; const y_adjust = desired_y_delta - y_delta; return [xmin - x_adjust / 2, ymin - y_adjust / 2, xmax + x_adjust / 2, ymax + y_adjust / 2]; } tms_to_wmts(x, y, z) { // Note this works both ways return [x, 2 ** z - 1 - y, z]; } wmts_to_tms(x, y, z) { // Note this works both ways return [x, 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] = meters_extent_to_geographic(bounds); return [minLon, minLat, maxLon, maxLat]; } get_tiles_by_extent(extent, level, tile_border = 1) { // skip calculation if any axis has undefined extent if (extent.some(value => !isFinite(value))) { return []; } // 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 = 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 * 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 / 2 ** z); } } //# sourceMappingURL=mercator_tile_source.js.map