UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

567 lines (454 loc) 14.3 kB
// @ts-nocheck import slugid from 'slugid'; import PixiTrack from './PixiTrack'; // Services import { tileProxy } from './services'; // Utils import { debounce } from './utils'; // Configs import { GLOBALS, ZOOM_DEBOUNCE } from './configs'; class OSMTilesTrack extends PixiTrack { /** * A track that must pull remote tiles * * @param scene: A PIXI.js scene to draw everything to. * @param server: The server to pull tiles from. * @param tilesetUid: The data set to get the tiles from the server */ constructor(context, options) { super(context, options); const { animate } = context; // Force OpenStreetMaps copyright // this.options.name = `© OpenStreetMap${options.name ? `\n${options.name}` : ''}`; // the tiles which should be visible (although they're not necessarily fetched) this.visibleTiles = new Set(); this.visibleTileIds = new Set(); // the tiles we already have requests out for this.fetching = new Set(); // tiles we have fetched and ready to be rendered this.fetchedTiles = {}; // the graphics that have already been drawn for this track this.tileGraphics = {}; this.minX = typeof this.options.minPos !== 'undefined' && !Number.isNaN(+this.options.minPos) ? +this.options.minPos : -180; this.maxX = +this.options.maxPos || 180; this.maxX = typeof this.options.maxPos !== 'undefined' && !Number.isNaN(+this.options.maxPos) ? +this.options.maxPos : 180; // HiGlass currently only supports squared tile sets but maybe in the // future... this.minY = this.options.minY || this.minX; this.maxY = this.options.maxY || this.maxX; this.maxZoom = 19; this.maxWidth = this.maxX - this.minX; this.animate = animate; this.uuid = slugid.nice(); this.refreshTilesDebounced = debounce( this.refreshTiles.bind(this), ZOOM_DEBOUNCE, ); } /** * Return the set of ids of all tiles which are both visible and fetched. */ visibleAndFetchedIds() { return Object.keys(this.fetchedTiles).filter((x) => this.visibleTileIds.has(x), ); } visibleAndFetchedTiles() { return this.visibleAndFetchedIds().map((x) => this.fetchedTiles[x]); } /** * Set which tiles are visible right now. * * @param tiles: A set of tiles which will be considered the currently visible * tile positions. */ setVisibleTiles(tilePositions) { this.visibleTiles = tilePositions.map((x) => ({ tileId: this.tileToLocalId(x), remoteId: this.tileToRemoteId(x), mirrored: x.mirrored, })); this.visibleTileIds = new Set(this.visibleTiles.map((x) => x.tileId)); } removeAllTiles() { const fetchedTileIDs = new Set(Object.keys(this.fetchedTiles)); this.removeTiles([...fetchedTileIDs]); } refreshTiles() { this.calculateVisibleTiles(); // tiles that are fetched const fetchedTileIDs = new Set(Object.keys(this.fetchedTiles)); // fetch the tiles that should be visible but haven't been fetched // and aren't in the process of being fetched const toFetch = [...this.visibleTiles].filter( (x) => !this.fetching.has(x.remoteId) && !fetchedTileIDs.has(x.tileId), ); for (let i = 0; i < toFetch.length; i++) { this.fetching.add(toFetch[i].remoteId); } // calculate which tiles are obsolete and remove them // fetchedTileID are remote ids const toRemove = [...fetchedTileIDs].filter( (x) => !this.visibleTileIds.has(x), ); this.removeTiles(toRemove); this.fetchNewTiles(toFetch); } /** * Remove obsolete tiles * * @param toRemoveIds: An array of tile ids to remove from the list of fetched tiles. */ removeTiles(toRemoveIds) { // if there's nothing to remove, don't bother doing anything if (!toRemoveIds.length) { return; } if (!this.areAllVisibleTilesLoaded()) { return; } toRemoveIds.forEach((x) => { const tileIdStr = x; this.destroyTile(this.fetchedTiles[tileIdStr]); if (tileIdStr in this.tileGraphics) { this.pMain.removeChild(this.tileGraphics[tileIdStr]); delete this.tileGraphics[tileIdStr]; } delete this.fetchedTiles[tileIdStr]; }); this.synchronizeTilesAndGraphics(); this.draw(); } /* * The local tile identifier. * @param {array} tile Contains `[zoomLevel, xPos, yPos]`. * @return {string} Remote ID string */ tileToLocalId(tile) { // tile contains return tile.join('.'); } /** * The tile identifier used on the server. * @param {array} tile Contains `[zoomLevel, xPos, yPos]`. * @return {string} Remote ID string */ tileToRemoteId(tile) { return tile.join('.'); } localToRemoteId(remoteId) { const idParts = remoteId.split('.'); return idParts.slice(0, idParts.length - 1).join('.'); } calculateZoomLevel() { const xZoomLevel = tileProxy.calculateZoomLevel( this._xScale, this.minX, this.maxX, ); const yZoomLevel = tileProxy.calculateZoomLevel( this._xScale, this.minY, this.maxY, ); let zoomLevel = Math.min(Math.max(xZoomLevel, yZoomLevel), this.maxZoom); if (this.options.maxZoom) { if (this.options.maxZoom >= 0) { zoomLevel = Math.min(this.options.maxZoom, zoomLevel); } else { console.error('Invalid maxZoom on track:', this); } } return zoomLevel; } calculateVisibleTiles() { // if we don't know anything about this dataset, no point // in trying to get tiles this.zoomLevel = this.calculateZoomLevel(); this.xTiles = tileProxy.calculateTiles( this.zoomLevel, this._xScale, this.minX, this.maxX, this.maxZoom, this.maxWidth, ); this.yTiles = tileProxy.calculateTiles( this.zoomLevel, this._yScale, this.minY, this.maxY, this.maxZoom, this.maxWidth, ); const rows = this.xTiles; const cols = this.yTiles; const zoomLevel = this.zoomLevel; // if we're mirroring tiles, then we only need tiles along the diagonal const tiles = []; // console.log('this.options:', this.options); // calculate the ids of the tiles that should be visible for (let i = 0; i < rows.length; i++) { for (let j = 0; j < cols.length; j++) { const newTile = [zoomLevel, rows[i], cols[j]]; tiles.push(newTile); } } this.setVisibleTiles(tiles); } zoomed(newXScale, newYScale, k, tx, ty) { super.zoomed(newXScale, newYScale); this.xScale(newXScale); this.yScale(newYScale); this.pMain.position.x = tx; // translateX; this.pMain.position.y = ty; // translateY; this.pMain.scale.x = k; // scaleX; this.pMain.scale.y = k; // scaleY; this.refreshTilesDebounced(); this.draw(); } setPosition(newPosition) { super.setPosition(newPosition); } setDimensions(newDimensions) { super.setDimensions(newDimensions); } /** * Check to see if all the visible tiles are loaded. * * If they are, remove all other tiles. */ areAllVisibleTilesLoaded() { // tiles that are fetched const fetchedTileIDs = new Set(Object.keys(this.fetchedTiles)); const visibleTileIdsList = [...this.visibleTileIds]; for (let i = 0; i < visibleTileIdsList.length; i++) { if (!fetchedTileIDs.has(visibleTileIdsList[i])) { return false; } } return true; } /** * Function is called when all tiles that should be visible have * been received. */ allTilesLoaded() {} minValue(_) { if (_) { this.scale.minValue = _; } return this.scale.minValue; } maxValue(_) { if (_) { this.scale.maxValue = _; } return this.scale.maxValue; } minRawValue() { // this is the minimum value from all the tiles that // hasn't been externally modified by locked scales return this.scale.minRawValue; } maxRawValue() { // this is the maximum value from all the tiles that // hasn't been externally modified by locked scales return this.scale.maxRawValue; } /** * Get the tile's position in its coordinate system. */ getTilePosAndDimensions(zoomLevel, tilePos) { const tileWidth = this.maxWidth / 2 ** zoomLevel; const tileHeight = tileWidth; const tileX = this.minX + tilePos[0] * tileWidth; const tileY = this.minY + tilePos[1] * tileHeight; return { tileX, tileY, tileWidth, tileHeight, }; } setSpriteProperties(sprite, zoomLevel, tilePos) { const { tileX, tileY, tileWidth, tileHeight } = this.getTilePosAndDimensions(zoomLevel, tilePos); sprite.x = this._refXScale(tileX); sprite.y = this._refYScale(tileY); const tileEndX = tileX + tileWidth; const tileEndY = tileY + tileHeight; sprite.width = this._refXScale(tileEndX) - this._refXScale(tileX); sprite.height = this._refYScale(tileEndY) - this._refYScale(tileY); } initTile(tile) { // create the tile // should be overwritten by child classes const texture = new GLOBALS.PIXI.Texture( new GLOBALS.PIXI.BaseTexture(tile.tileData.img), ); const sprite = new GLOBALS.PIXI.Sprite(texture); const graphics = tile.graphics; tile.sprite = sprite; this.setSpriteProperties( tile.sprite, tile.tileData.zoomLevel, tile.tileData.tilePos, ); graphics.removeChildren(); graphics.addChild(tile.sprite); } updateTile(tile) { // console.log("ERROR: unimplemented updateTile:", this); } destroyTile(tile) { // remove all data structures needed to draw this tile } /** * Add graphics for tiles that have no graphics */ addMissingGraphics() { const fetchedTileIDs = Object.keys(this.fetchedTiles); for (let i = 0; i < fetchedTileIDs.length; i++) { if (!(fetchedTileIDs[i] in this.tileGraphics)) { const newGraphics = new GLOBALS.PIXI.Graphics(); this.pMain.addChild(newGraphics); this.fetchedTiles[fetchedTileIDs[i]].graphics = newGraphics; this.initTile(this.fetchedTiles[fetchedTileIDs[i]]); this.tileGraphics[fetchedTileIDs[i]] = newGraphics; } } } /** * Change the graphics for existing tiles */ updateExistingGraphics() { const fetchedTileIDs = Object.keys(this.fetchedTiles); for (let i = 0; i < fetchedTileIDs.length; i++) { this.updateTile(this.fetchedTiles[fetchedTileIDs[i]]); } } /** * Make sure that we have a one to one mapping between tiles * and graphics objects */ synchronizeTilesAndGraphics() { // keep track of which tiles are visible at the moment this.addMissingGraphics(); this.updateExistingGraphics(); } /** * Extract drawable data from a tile loaded by a generic tile loader * * @param tile: A tile returned by a TiledArea. * @param dataLoader: A function for extracting drawable data from a tile. This * usually means differentiating the between dense and sparse * tiles and putting the data into an array. */ loadTileData(tile, dataLoader) { // see if the data is already cached let loadedTileData = this.lruCache.get(tile.tileId); // if not, load it and put it in the cache if (!loadedTileData) { loadedTileData = dataLoader(tile.data, tile.type); this.lruCache.put(tile.tileId, loadedTileData); } return loadedTileData; } /** * Get the url used to fetch the tile data */ getTileUrl(tileZxy) { const serverPrefixes = ['a', 'b', 'c']; const serverPrefixIndex = Math.floor(Math.random() * serverPrefixes.length); const src = `https://${serverPrefixes[serverPrefixIndex]}.tile.openstreetmap.org/${tileZxy[0]}/${tileZxy[1]}/${tileZxy[2]}.png`; return src; } fetchNewTiles(toFetch) { if (toFetch.length > 0) { const toFetchList = [...new Set(toFetch.map((x) => x.remoteId))]; for (const tileId of toFetchList) { const parts = tileId.split('.'); const src = this.getTileUrl(parts); const img = new Image(); img.crossOrigin = 'Anonymous'; img.src = src; img.onload = () => { const loadedTiles = {}; loadedTiles[tileId] = { tileId, img, zoomLevel: +parts[0], tilePos: [+parts[1], +parts[2]], tileSrc: src, }; this.receivedTiles(loadedTiles); }; } } } /** * We've gotten a bunch of tiles from the server in * response to a request from fetchTiles. */ receivedTiles(loadedTiles) { for (let i = 0; i < this.visibleTiles.length; i++) { const tileId = this.visibleTiles[i].tileId; if (!loadedTiles[this.visibleTiles[i].remoteId]) continue; if (this.visibleTiles[i].remoteId in loadedTiles) { if (!(tileId in this.fetchedTiles)) { // this tile may have graphics associated with it this.fetchedTiles[tileId] = this.visibleTiles[i]; } this.fetchedTiles[tileId].tileData = loadedTiles[this.visibleTiles[i].remoteId]; } } for (const key in loadedTiles) { if (loadedTiles[key]) { if (this.fetching.has(key)) { this.fetching.delete(key); } } } this.synchronizeTilesAndGraphics(); // Mainly called to remove old unnecessary tiles this.refreshTiles(); // we need to draw when we receive new data this.draw(); // Let HiGlass know we need to re-render this.animate(); } draw() { if (this.delayDrawing) return; super.draw(); } /** * Draw a tile on some graphics */ drawTile() {} refScalesChanged(refXScale, refYScale) { super.refScalesChanged(refXScale, refYScale); for (const uid in this.fetchedTiles) { const tile = this.fetchedTiles[uid]; if (tile.sprite) { this.setSpriteProperties( tile.sprite, tile.tileData.zoomLevel, tile.tileData.tilePos, ); } else { // console.log('skipping...', tile.tileId); } } } } export default OSMTilesTrack;