UNPKG

esri-leaflet

Version:

Leaflet plugins for consuming ArcGIS Online and ArcGIS Server services.

567 lines (459 loc) 15.2 kB
import { LatLngBounds, toLatLngBounds as latLngBounds, Layer, Browser, Util, Point, Bounds } from 'leaflet'; export var FeatureGrid = Layer.extend({ // @section // @aka GridLayer options options: { // @option cellSize: Number|Point = 256 // Width and height of cells in the grid. Use a number if width and height are equal, or `L.point(width, height)` otherwise. cellSize: 512, // @option updateWhenIdle: Boolean = (depends) // Load new cells only when panning ends. // `true` by default on mobile browsers, in order to avoid too many requests and keep smooth navigation. // `false` otherwise in order to display new cells _during_ panning, since it is easy to pan outside the // [`keepBuffer`](#gridlayer-keepbuffer) option in desktop browsers. updateWhenIdle: Browser.mobile, // @option updateInterval: Number = 150 // Cells will not update more than once every `updateInterval` milliseconds when panning. updateInterval: 150, // @option noWrap: Boolean = false // Whether the layer is wrapped around the antimeridian. If `true`, the // GridLayer will only be displayed once at low zoom levels. Has no // effect when the [map CRS](#map-crs) doesn't wrap around. Can be used // in combination with [`bounds`](#gridlayer-bounds) to prevent requesting // cells outside the CRS limits. noWrap: false, // @option keepBuffer: Number = 1.5 // When panning the map, keep this many rows and columns of cells before unloading them. keepBuffer: 1.5 }, initialize: function (options) { Util.setOptions(this, options); }, onAdd: function (map) { this._cells = {}; this._activeCells = {}; this._resetView(); this._update(); }, onRemove: function (map) { this._removeAllCells(); this._cellZoom = undefined; }, // @method isLoading: Boolean // Returns `true` if any cell in the grid layer has not finished loading. isLoading: function () { return this._loading; }, // @method redraw: this // Causes the layer to clear all the cells and request them again. redraw: function () { if (this._map) { this._removeAllCells(); this._update(); } return this; }, getEvents: function () { var events = { viewprereset: this._invalidateAll, viewreset: this._resetView, zoom: this._resetView, moveend: this._onMoveEnd }; if (!this.options.updateWhenIdle) { // update cells on move, but not more often than once per given interval if (!this._onMove) { this._onMove = Util.throttle( this._onMoveEnd, this.options.updateInterval, this ); } events.move = this._onMove; } return events; }, // @section Extension methods // Layers extending `GridLayer` shall reimplement the following method. // @method createCell(coords: Object, done?: Function): HTMLElement // Called only internally, must be overridden by classes extending `GridLayer`. // Returns the `HTMLElement` corresponding to the given `coords`. If the `done` callback // is specified, it must be called when the cell has finished loading and drawing. createCell: function () { return document.createElement('div'); }, removeCell: function () { }, reuseCell: function () { }, cellLeave: function () { }, cellEnter: function () { }, // @section // @method getCellSize: Point // Normalizes the [cellSize option](#gridlayer-cellsize) into a point. Used by the `createCell()` method. getCellSize: function () { var s = this.options.cellSize; return s instanceof Point ? s : new Point(s, s); }, _pruneCells: function () { if (!this._map) { return; } var key, cell; for (key in this._cells) { cell = this._cells[key]; cell.retain = cell.current; } for (key in this._cells) { cell = this._cells[key]; if (cell.current && !cell.active) { var coords = cell.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 (key in this._cells) { if (!this._cells[key].retain) { this._removeCell(key); } } }, _removeAllCells: function () { for (var key in this._cells) { this._removeCell(key); } }, _invalidateAll: function () { this._removeAllCells(); this._cellZoom = undefined; }, _retainParent: function (x, y, z, minZoom) { var x2 = Math.floor(x / 2); var y2 = Math.floor(y / 2); var z2 = z - 1; var coords2 = new Point(+x2, +y2); coords2.z = +z2; var key = this._cellCoordsToKey(coords2); var cell = this._cells[key]; if (cell && cell.active) { cell.retain = true; return true; } else if (cell && cell.loaded) { cell.retain = true; } if (z2 > minZoom) { return this._retainParent(x2, y2, z2, minZoom); } return false; }, _retainChildren: function (x, y, z, maxZoom) { for (var i = 2 * x; i < 2 * x + 2; i++) { for (var j = 2 * y; j < 2 * y + 2; j++) { var coords = new Point(i, j); coords.z = z + 1; var key = this._cellCoordsToKey(coords); var cell = this._cells[key]; if (cell && cell.active) { cell.retain = true; continue; } else if (cell && cell.loaded) { cell.retain = true; } if (z + 1 < maxZoom) { this._retainChildren(i, j, z + 1, maxZoom); } } } }, _resetView: function (e) { var animating = e && (e.pinch || e.flyTo); if (animating) { return; } this._setView( this._map.getCenter(), this._map.getZoom(), animating, animating ); }, _setView: function (center, zoom, noPrune, noUpdate) { var cellZoom = Math.round(zoom); if (!noUpdate) { this._cellZoom = cellZoom; if (this._abortLoading) { this._abortLoading(); } this._resetGrid(); if (cellZoom !== undefined) { this._update(center); } if (!noPrune) { this._pruneCells(); } // Flag to prevent _updateOpacity from pruning cells during // a zoom anim or a pinch gesture this._noPrune = !!noPrune; } }, _resetGrid: function () { var map = this._map; var crs = map.options.crs; var cellSize = (this._cellSize = this.getCellSize()); var cellZoom = this._cellZoom; var bounds = this._map.getPixelWorldBounds(this._cellZoom); if (bounds) { this._globalCellRange = this._pxBoundsToCellRange(bounds); } this._wrapX = crs.wrapLng && !this.options.noWrap && [ Math.floor(map.project([0, crs.wrapLng[0]], cellZoom).x / cellSize.x), Math.ceil(map.project([0, crs.wrapLng[1]], cellZoom).x / cellSize.y) ]; this._wrapY = crs.wrapLat && !this.options.noWrap && [ Math.floor(map.project([crs.wrapLat[0], 0], cellZoom).y / cellSize.x), Math.ceil(map.project([crs.wrapLat[1], 0], cellZoom).y / cellSize.y) ]; }, _onMoveEnd: function (e) { var animating = e && (e.pinch || e.flyTo); if (animating || !this._map || this._map._animatingZoom) { return; } this._update(); }, _getCelldPixelBounds: function (center) { var map = this._map; var mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(); var scale = map.getZoomScale(mapZoom, this._cellZoom); var pixelCenter = map.project(center, this._cellZoom).floor(); var halfSize = map.getSize().divideBy(scale * 2); return new Bounds( pixelCenter.subtract(halfSize), pixelCenter.add(halfSize) ); }, // Private method to load cells in the grid's active zoom level according to map bounds _update: function (center) { var map = this._map; if (!map) { return; } var zoom = Math.round(map.getZoom()); if (center === undefined) { center = map.getCenter(); } var pixelBounds = this._getCelldPixelBounds(center); var cellRange = this._pxBoundsToCellRange(pixelBounds); var cellCenter = cellRange.getCenter(); var queue = []; var margin = this.options.keepBuffer; var noPruneRange = new Bounds( cellRange.getBottomLeft().subtract([margin, -margin]), cellRange.getTopRight().add([margin, -margin]) ); // Sanity check: panic if the cell range contains Infinity somewhere. if ( !( isFinite(cellRange.min.x) && isFinite(cellRange.min.y) && isFinite(cellRange.max.x) && isFinite(cellRange.max.y) ) ) { throw new Error('Attempted to load an infinite number of cells'); } for (var key in this._cells) { var c = this._cells[key].coords; if ( c.z !== this._cellZoom || !noPruneRange.contains(new Point(c.x, c.y)) ) { this._cells[key].current = false; } } // _update just loads more cells. If the cell zoom level differs too much // from the map's, let _setView reset levels and prune old cells. if (Math.abs(zoom - this._cellZoom) > 1) { this._setView(center, zoom); return; } // create a queue of coordinates to load cells from for (var j = cellRange.min.y; j <= cellRange.max.y; j++) { for (var i = cellRange.min.x; i <= cellRange.max.x; i++) { var coords = new Point(i, j); coords.z = this._cellZoom; if (!this._isValidCell(coords)) { continue; } var cell = this._cells[this._cellCoordsToKey(coords)]; if (cell) { cell.current = true; } else { queue.push(coords); } } } // sort cell queue to load cells in order of their distance to center queue.sort(function (a, b) { return a.distanceTo(cellCenter) - b.distanceTo(cellCenter); }); if (queue.length !== 0) { // if it's the first batch of cells to load if (!this._loading) { this._loading = true; } for (i = 0; i < queue.length; i++) { var _key = this._cellCoordsToKey(queue[i]); var _coords = this._keyToCellCoords(_key); if (this._activeCells[_coords]) { this._reuseCell(queue[i]); } else { this._createCell(queue[i]); } } } }, _isValidCell: function (coords) { var crs = this._map.options.crs; if (!crs.infinite) { // don't load cell if it's out of bounds and not wrapped var bounds = this._globalCellRange; 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; } // don't load cell if it doesn't intersect the bounds in options var cellBounds = this._cellCoordsToBounds(coords); return latLngBounds(this.options.bounds).overlaps(cellBounds); }, _keyToBounds: function (key) { return this._cellCoordsToBounds(this._keyToCellCoords(key)); }, _cellCoordsToNwSe: function (coords) { var map = this._map; var cellSize = this.getCellSize(); var nwPoint = coords.scaleBy(cellSize); var sePoint = nwPoint.add(cellSize); var nw = map.unproject(nwPoint, coords.z); var se = map.unproject(sePoint, coords.z); return [nw, se]; }, // converts cell coordinates to its geographical bounds _cellCoordsToBounds: function (coords) { var bp = this._cellCoordsToNwSe(coords); var bounds = new LatLngBounds(bp[0], bp[1]); if (!this.options.noWrap) { bounds = this._map.wrapLatLngBounds(bounds); } return bounds; }, // converts cell coordinates to key for the cell cache _cellCoordsToKey: function (coords) { return coords.x + ':' + coords.y + ':' + coords.z; }, // converts cell cache key to coordinates _keyToCellCoords: function (key) { var k = key.split(':'); var coords = new Point(+k[0], +k[1]); coords.z = +k[2]; return coords; }, _removeCell: function (key) { var cell = this._cells[key]; if (!cell) { return; } var coords = this._keyToCellCoords(key); var wrappedCoords = this._wrapCoords(coords); var cellBounds = this._cellCoordsToBounds(this._wrapCoords(coords)); cell.current = false; delete this._cells[key]; this._activeCells[key] = cell; this.cellLeave(cellBounds, wrappedCoords, key); this.fire('cellleave', { key: key, coords: wrappedCoords, bounds: cellBounds }); }, _reuseCell: function (coords) { var key = this._cellCoordsToKey(coords); // save cell in cache this._cells[key] = this._activeCells[key]; this._cells[key].current = true; var wrappedCoords = this._wrapCoords(coords); var cellBounds = this._cellCoordsToBounds(this._wrapCoords(coords)); this.cellEnter(cellBounds, wrappedCoords, key); this.fire('cellenter', { key: key, coords: wrappedCoords, bounds: cellBounds }); }, _createCell: function (coords) { var key = this._cellCoordsToKey(coords); var wrappedCoords = this._wrapCoords(coords); var cellBounds = this._cellCoordsToBounds(this._wrapCoords(coords)); this.createCell(cellBounds, wrappedCoords, key); this.fire('cellcreate', { key: key, coords: wrappedCoords, bounds: cellBounds }); // save cell in cache this._cells[key] = { coords: coords, current: true }; Util.requestAnimFrame(this._pruneCells, this); }, _cellReady: function (coords, err, cell) { var key = this._cellCoordsToKey(coords); cell = this._cells[key]; if (!cell) { return; } cell.loaded = +new Date(); cell.active = true; }, _getCellPos: function (coords) { return coords.scaleBy(this.getCellSize()); }, _wrapCoords: function (coords) { var newCoords = new Point( this._wrapX ? Util.wrapNum(coords.x, this._wrapX) : coords.x, this._wrapY ? Util.wrapNum(coords.y, this._wrapY) : coords.y ); newCoords.z = coords.z; return newCoords; }, _pxBoundsToCellRange: function (bounds) { var cellSize = this.getCellSize(); return new Bounds( bounds.min.unscaleBy(cellSize).floor(), bounds.max.unscaleBy(cellSize).ceil().subtract([1, 1]) ); } });