maplair
Version:
JavaScript library for mobile-friendly interactive maps
447 lines (446 loc) • 15.4 kB
JavaScript
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