UNPKG

maplair

Version:

JavaScript library for mobile-friendly interactive maps

447 lines (446 loc) • 15.4 kB
import { falseFn, setOptions, throttle, wrapNum } from "../../core/Util.js"; import { Point } from "../../geometry/Point.js"; import Browser_default from "../../core/Browser.js"; import { create, setPosition, setTransform, toBack, toFront } from "../../dom/DomUtil.js"; import { Bounds } from "../../geometry/Bounds.js"; import { LatLngBounds } from "../../geo/LatLngBounds.js"; import { Layer } from "../Layer.js"; //#region src/layer/tile/GridLayer.js var GridLayer = class GridLayer extends Layer { static { this.setDefaultOptions({ tileSize: 256, opacity: 1, updateWhenIdle: !Browser_default.reducedMotion, updateWhenZooming: true, updateInterval: Browser_default.reducedMotion ? 5e3 : 200, zIndex: 1, bounds: null, minZoom: 0, maxZoom: void 0, maxNativeZoom: void 0, minNativeZoom: void 0, noWrap: false, pane: "tilePane", className: "", keepBuffer: 2 }); } constructor(options) { super(); setOptions(this, options); this.callInitHooks(GridLayer); } onAdd() { this._initContainer(); this._levels = {}; this._tiles = {}; this._resetView(); } beforeAdd(map) { map._addZoomLimit(this); } onRemove(map) { this._removeAllTiles(); this._container.remove(); map._removeZoomLimit(this); this._container = null; this._tileZoom = void 0; clearTimeout(this._pruneTimeout); } bringToFront() { if (this._map) { toFront(this._container); this._setAutoZIndex(Math.max); } return this; } bringToBack() { if (this._map) { toBack(this._container); this._setAutoZIndex(Math.min); } return this; } getContainer() { return this._container; } setOpacity(opacity) { this.options.opacity = opacity; this._updateOpacity(); return this; } setZIndex(zIndex) { this.options.zIndex = zIndex; this._updateZIndex(); return this; } isLoading() { return this._loading; } redraw() { if (this._map) { this._removeAllTiles(); const tileZoom = this._clampZoom(this._map.getZoom()); if (tileZoom !== this._tileZoom) { this._tileZoom = tileZoom; this._updateLevels(); } this._update(); } return this; } getEvents() { const events = { viewprereset: this._invalidateAll, viewreset: this._resetView, zoom: this._resetView, moveend: this._onMoveEnd }; if (!this.options.updateWhenIdle) { if (!this._onMove) this._onMove = throttle(this._onMoveEnd, this.options.updateInterval, this); events.move = this._onMove; } if (this._zoomAnimated) events.zoomanim = this._animateZoom; return events; } createTile() { return document.createElement("div"); } getTileSize() { const s = this.options.tileSize; return s instanceof Point ? s : new Point(s, s); } _updateZIndex() { if (this._container && this.options.zIndex !== void 0 && this.options.zIndex !== null) this._container.style.zIndex = this.options.zIndex; } _setAutoZIndex(compare) { const layers = this.getPane().children; let edgeZIndex = -compare(-Infinity, Infinity); for (const layer of layers) { const zIndex = layer.style.zIndex; if (layer !== this._container && zIndex) edgeZIndex = compare(edgeZIndex, +zIndex); } if (isFinite(edgeZIndex)) { this.options.zIndex = edgeZIndex + compare(-1, 1); this._updateZIndex(); } } _updateOpacity() { if (!this._map) return; this._container.style.opacity = this.options.opacity; const now = Date.now(); let nextFrame = false, willPrune = false; for (const tile of Object.values(this._tiles ?? {})) { if (!tile.current || !tile.loaded) continue; const fade = Math.min(1, (now - tile.loaded) / 200); tile.el.style.opacity = fade; if (fade < 1) nextFrame = true; else { if (tile.active) willPrune = true; else this._onOpaqueTile(tile); tile.active = true; } } if (willPrune && !this._noPrune) this._pruneTiles(); if (nextFrame) { cancelAnimationFrame(this._fadeFrame); this._fadeFrame = requestAnimationFrame(this._updateOpacity.bind(this)); } } _onOpaqueTile() {} _initContainer() { if (this._container) return; this._container = create("div", `maplair-layer ${this.options.className ?? ""}`); this._updateZIndex(); if (this.options.opacity < 1) this._updateOpacity(); this.getPane().appendChild(this._container); } _updateLevels() { const zoom = this._tileZoom, maxZoom = this.options.maxZoom; if (zoom === void 0) return; for (let z of Object.keys(this._levels)) { z = Number(z); if (this._levels[z].el.children.length || z === zoom) { this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z); this._onUpdateLevel(z); } else { this._levels[z].el.remove(); this._removeTilesAtZoom(z); this._onRemoveLevel(z); delete this._levels[z]; } } let level = this._levels[zoom]; const map = this._map; if (!level) { level = this._levels[zoom] = {}; level.el = create("div", "maplair-tile-container maplair-zoom-animated", this._container); level.el.style.zIndex = maxZoom; level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round(); level.zoom = zoom; this._setZoomTransform(level, map.getCenter(), map.getZoom()); falseFn(level.el.offsetWidth); this._onCreateLevel(level); } this._level = level; return level; } _onUpdateLevel() {} _onRemoveLevel() {} _onCreateLevel() {} _pruneTiles() { if (!this._map) return; const zoom = this._map.getZoom(); if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { this._removeAllTiles(); return; } for (const tile of Object.values(this._tiles)) tile.retain = tile.current; for (const tile of Object.values(this._tiles)) if (tile.current && !tile.active) { const coords = tile.coords; if (!this._retainParent(coords.x, coords.y, coords.z, coords.z - 5)) this._retainChildren(coords.x, coords.y, coords.z, coords.z + 2); } for (const [key, tile] of Object.entries(this._tiles)) if (!tile.retain) this._removeTile(key); } _removeTilesAtZoom(zoom) { for (const [key, tile] of Object.entries(this._tiles)) if (tile.coords.z === zoom) this._removeTile(key); } _removeAllTiles() { for (const key of Object.keys(this._tiles)) this._removeTile(key); } _invalidateAll() { for (const z of Object.keys(this._levels)) { this._levels[z].el.remove(); this._onRemoveLevel(Number(z)); delete this._levels[z]; } this._removeAllTiles(); this._tileZoom = void 0; } _retainParent(x, y, z, minZoom) { const x2 = Math.floor(x / 2), y2 = Math.floor(y / 2), z2 = z - 1, coords2 = new Point(+x2, +y2); coords2.z = +z2; const key = this._tileCoordsToKey(coords2), tile = this._tiles[key]; if (tile?.active) { tile.retain = true; return true; } else if (tile?.loaded) tile.retain = true; if (z2 > minZoom) return this._retainParent(x2, y2, z2, minZoom); return false; } _retainChildren(x, y, z, maxZoom) { for (let i = 2 * x; i < 2 * x + 2; i++) for (let j = 2 * y; j < 2 * y + 2; j++) { const coords = new Point(i, j); coords.z = z + 1; const key = this._tileCoordsToKey(coords), tile = this._tiles[key]; if (tile?.active) { tile.retain = true; continue; } else if (tile?.loaded) tile.retain = true; if (z + 1 < maxZoom) this._retainChildren(i, j, z + 1, maxZoom); } } _resetView(e) { const animating = e && (e.pinch || e.flyTo); this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating); } _animateZoom(e) { this._setView(e.center, e.zoom, true, e.noUpdate); } _clampZoom(zoom) { const options = this.options; if (void 0 !== options.minNativeZoom && zoom < options.minNativeZoom) return options.minNativeZoom; if (void 0 !== options.maxNativeZoom && options.maxNativeZoom < zoom) return options.maxNativeZoom; return zoom; } _setView(center, zoom, noPrune, noUpdate) { let tileZoom = Math.round(zoom); if (this.options.maxZoom !== void 0 && tileZoom > this.options.maxZoom || this.options.minZoom !== void 0 && tileZoom < this.options.minZoom) tileZoom = void 0; else tileZoom = this._clampZoom(tileZoom); const tileZoomChanged = this.options.updateWhenZooming && tileZoom !== this._tileZoom; if (!noUpdate || tileZoomChanged) { this._tileZoom = tileZoom; if (this._abortLoading) this._abortLoading(); this._updateLevels(); this._resetGrid(); if (tileZoom !== void 0) this._update(center); if (!noPrune) this._pruneTiles(); this._noPrune = !!noPrune; } this._setZoomTransforms(center, zoom); } _setZoomTransforms(center, zoom) { for (const level of Object.values(this._levels)) this._setZoomTransform(level, center, zoom); } _setZoomTransform(level, center, zoom) { const scale = this._map.getZoomScale(zoom, level.zoom), translate = level.origin.multiplyBy(scale).subtract(this._map._getNewPixelOrigin(center, zoom)).round(); setTransform(level.el, translate, scale); } _resetGrid() { const map = this._map, crs = map.options.crs, tileSize = this._tileSize = this.getTileSize(), tileZoom = this._tileZoom; const bounds = this._map.getPixelWorldBounds(this._tileZoom); if (bounds) this._globalTileRange = this._pxBoundsToTileRange(bounds); this._wrapX = crs.wrapLng && !this.options.noWrap && [Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x), Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)]; this._wrapY = crs.wrapLat && !this.options.noWrap && [Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x), Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)]; } _onMoveEnd() { if (!this._map || this._map._animatingZoom) return; this._update(); } _getTiledPixelBounds(center) { const map = this._map, mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(), scale = map.getZoomScale(mapZoom, this._tileZoom), pixelCenter = map.project(center, this._tileZoom).floor(), halfSize = map.getSize().divideBy(scale * 2); return new Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize)); } _update(center) { const map = this._map; if (!map) return; const zoom = this._clampZoom(map.getZoom()); if (center === void 0) center = map.getCenter(); if (this._tileZoom === void 0) return; const pixelBounds = this._getTiledPixelBounds(center), tileRange = this._pxBoundsToTileRange(pixelBounds), tileCenter = tileRange.getCenter(), queue = [], margin = this.options.keepBuffer, noPruneRange = new Bounds(tileRange.getBottomLeft().subtract([margin, -margin]), tileRange.getTopRight().add([margin, -margin])); if (!(isFinite(tileRange.min.x) && isFinite(tileRange.min.y) && isFinite(tileRange.max.x) && isFinite(tileRange.max.y))) throw new Error("Attempted to load an infinite number of tiles"); for (const tile of Object.values(this._tiles)) { const c = tile.coords; if (c.z !== this._tileZoom || !noPruneRange.contains(new Point(c.x, c.y))) tile.current = false; } if (Math.abs(zoom - this._tileZoom) > 1) { this._setView(center, zoom); return; } for (let j = tileRange.min.y; j <= tileRange.max.y; j++) for (let i = tileRange.min.x; i <= tileRange.max.x; i++) { const coords = new Point(i, j); coords.z = this._tileZoom; if (!this._isValidTile(coords)) continue; const tile = this._tiles[this._tileCoordsToKey(coords)]; if (tile) tile.current = true; else queue.push(coords); } queue.sort((a, b) => a.distanceTo(tileCenter) - b.distanceTo(tileCenter)); if (queue.length !== 0) { if (!this._loading) { this._loading = true; this.fire("loading"); } const fragment = document.createDocumentFragment(); for (const q of queue) this._addTile(q, fragment); this._level.el.appendChild(fragment); } } _isValidTile(coords) { const crs = this._map.options.crs; if (!crs.infinite) { const bounds = this._globalTileRange; if (!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x) || !crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y)) return false; } if (!this.options.bounds) return true; const tileBounds = this._tileCoordsToBounds(coords); return new LatLngBounds(this.options.bounds).overlaps(tileBounds); } _keyToBounds(key) { return this._tileCoordsToBounds(this._keyToTileCoords(key)); } _tileCoordsToNwSe(coords) { const map = this._map, tileSize = this.getTileSize(), nwPoint = coords.scaleBy(tileSize), sePoint = nwPoint.add(tileSize); return [map.unproject(nwPoint, coords.z), map.unproject(sePoint, coords.z)]; } _tileCoordsToBounds(coords) { const bp = this._tileCoordsToNwSe(coords); let bounds = new LatLngBounds(bp[0], bp[1]); if (!this.options.noWrap) bounds = this._map.wrapLatLngBounds(bounds); return bounds; } _tileCoordsToKey(coords) { return `${coords.x}:${coords.y}:${coords.z}`; } _keyToTileCoords(key) { const k = key.split(":"), coords = new Point(+k[0], +k[1]); coords.z = +k[2]; return coords; } _removeTile(key) { const tile = this._tiles[key]; if (!tile) return; tile.el.remove(); delete this._tiles[key]; this.fire("tileunload", { tile: tile.el, coords: this._keyToTileCoords(key) }); } _initTile(tile) { tile.classList.add("maplair-tile"); const tileSize = this.getTileSize(); tile.style.width = `${tileSize.x}px`; tile.style.height = `${tileSize.y}px`; tile.onselectstart = falseFn; tile.onpointermove = falseFn; } _addTile(coords, container) { const tilePos = this._getTilePos(coords), key = this._tileCoordsToKey(coords); const tile = this.createTile(this._wrapCoords(coords), this._tileReady.bind(this, coords)); this._initTile(tile); if (this.createTile.length < 2) requestAnimationFrame(this._tileReady.bind(this, coords, null, tile)); setPosition(tile, tilePos); this._tiles[key] = { el: tile, coords, current: true }; container.appendChild(tile); this.fire("tileloadstart", { tile, coords }); } _tileReady(coords, err, tile) { if (err) this.fire("tileerror", { error: err, tile, coords }); const key = this._tileCoordsToKey(coords); tile = this._tiles[key]; if (!tile) return; tile.loaded = Date.now(); if (this._map._fadeAnimated) { tile.el.style.opacity = 0; cancelAnimationFrame(this._fadeFrame); this._fadeFrame = requestAnimationFrame(this._updateOpacity.bind(this)); } else { tile.active = true; this._pruneTiles(); } if (!err) { tile.el.classList.add("maplair-tile-loaded"); this.fire("tileload", { tile: tile.el, coords }); } if (this._noTilesToLoad()) { this._loading = false; this.fire("load"); if (!this._map._fadeAnimated) requestAnimationFrame(this._pruneTiles.bind(this)); else this._pruneTimeout = setTimeout(this._pruneTiles.bind(this), 250); } } _getTilePos(coords) { return coords.scaleBy(this.getTileSize()).subtract(this._level.origin); } _wrapCoords(coords) { const newCoords = new Point(this._wrapX ? wrapNum(coords.x, this._wrapX) : coords.x, this._wrapY ? wrapNum(coords.y, this._wrapY) : coords.y); newCoords.z = coords.z; return newCoords; } _pxBoundsToTileRange(bounds) { const tileSize = this.getTileSize(); return new Bounds(bounds.min.unscaleBy(tileSize).floor(), bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1])); } _noTilesToLoad() { return Object.values(this._tiles).every((t) => t.loaded); } }; //#endregion export { GridLayer }; //# sourceMappingURL=GridLayer.js.map