index.js (7336B)
1 import L from 'leaflet'; 2 3 import {fetch} from '~/lib/xhr-promise'; 4 import './style.css'; 5 6 class DataTileStatus { 7 static STATUS_LOADING = 'LOADING'; 8 static STATUS_ERROR = 'ERROR'; 9 static STATUS_NO_DATA = 'ND'; 10 static STATUS_OK = 'OK'; 11 } 12 13 function decodeElevations(arrBuf) { 14 const array = new Int16Array(arrBuf); 15 for (let i = 1; i < array.length; i++) { 16 array[i] += array[i - 1]; 17 } 18 return array; 19 } 20 21 function mod(x, n) { 22 return ((x % n) + n) % n; 23 } 24 25 const ElevationLayer = L.TileLayer.extend({ 26 options: { 27 maxNativeZoom: 11, 28 tileSize: 256, 29 noDataValue: -512, 30 }, 31 32 initialize: function (url, options) { 33 L.TileLayer.prototype.initialize.call(this, url, options); 34 this.label = L.tooltip( 35 { 36 direction: 'bottom', 37 className: 'elevation-display-label', 38 offset: [0, 6], 39 }, 40 null 41 ); 42 }, 43 44 onAdd: function (map) { 45 L.TileLayer.prototype.onAdd.call(this, map); 46 this.setupElements(true); 47 this.setupEvents(true); 48 }, 49 50 onRemove: function (map) { 51 this.setupEvents(false); 52 this.setupElements(false); 53 map.removeLayer(this.label); 54 L.TileLayer.prototype.onRemove.call(this, map); 55 }, 56 57 setupElements: function (enable) { 58 const classFunc = enable ? 'addClass' : 'removeClass'; 59 L.DomUtil[classFunc](this._container, 'highlight'); 60 L.DomUtil[classFunc](this._map._container, 'elevation-display-control-active'); 61 }, 62 63 setupEvents: function (on) { 64 const eventFunc = on ? 'on' : 'off'; 65 66 this[eventFunc]( 67 { 68 tileunload: this.onTileUnload, 69 tileload: this.onTileLoad, 70 }, 71 this 72 ); 73 this._map[eventFunc]( 74 { 75 mousemove: this.onMouseMove, 76 mouseover: this.onMouseMove, 77 mouseout: this.onMouseOut, 78 zoomend: this.onZoomEnd, 79 }, 80 this 81 ); 82 }, 83 84 onTileLoad: function () { 85 if (this._mousePos) { 86 this.updateElevationDisplay(this._map.layerPointToLatLng(this._mousePos)); 87 } 88 }, 89 90 onTileUnload: function (ev) { 91 ev.tile._data.request.abort(); 92 delete ev.tile._data; 93 }, 94 95 createTile: function (coords, done) { 96 const tile = L.DomUtil.create('div'); 97 tile._data = {}; 98 tile._data.status = DataTileStatus.STATUS_LOADING; 99 tile._data.request = fetch(this.getTileUrl(coords), { 100 responseType: 'arraybuffer', 101 isResponseSuccess: (xhr) => [200, 404].includes(xhr.status), 102 }); 103 tile._data.request.then( 104 (xhr) => this.onDataLoad(xhr, tile, done), 105 (error) => this.onDataLoadError(tile, done, error) 106 ); 107 return tile; 108 }, 109 110 onDataLoad: function (xhr, tile, done) { 111 if (xhr.status === 200) { 112 tile._data.elevations = decodeElevations(xhr.response); 113 tile._data.status = DataTileStatus.STATUS_OK; 114 } else { 115 tile._data.status = DataTileStatus.STATUS_NO_DATA; 116 } 117 done(null, tile); 118 }, 119 120 onDataLoadError: function (tile, done, error) { 121 done(error, done); 122 tile._data.status = DataTileStatus.STATUS_ERROR; 123 }, 124 125 getElevationAtLayerPoint: function (layerPoint) { 126 const tileSize = this.getTileSize(); 127 const coordsScale = tileSize.x / this.options.tileSize; 128 const tileCoords = { 129 x: Math.floor(layerPoint.x / tileSize.x), 130 y: Math.floor(layerPoint.y / tileSize.y), 131 z: this._map.getZoom(), 132 }; 133 const tileKey = this._tileCoordsToKey(tileCoords); 134 const tile = this._tiles[tileKey]; 135 if (!tile) { 136 return {ready: false}; 137 } 138 const tileData = tile.el._data; 139 if (tileData.status === DataTileStatus.STATUS_LOADING) { 140 return {ready: false}; 141 } 142 if (tileData.status === DataTileStatus.STATUS_ERROR) { 143 return { 144 ready: true, 145 error: true, 146 }; 147 } 148 if (tileData.status === DataTileStatus.STATUS_NO_DATA) { 149 return {ready: true, elevation: null}; 150 } 151 const dataCoords = { 152 x: Math.floor(mod(layerPoint.x, tileSize.x) / coordsScale), 153 y: Math.floor(mod(layerPoint.y, tileSize.y) / coordsScale), 154 }; 155 let elevation = tileData.elevations[dataCoords.y * this.options.tileSize + dataCoords.x]; 156 if (elevation === this.options.noDataValue) { 157 elevation = null; 158 } 159 return {ready: true, elevation}; 160 }, 161 162 bilinearInterpolate: function (values, dx, dy) { 163 const [v1, v2, v3, v4] = values; 164 const q1 = v1 * (1 - dx) + v2 * dx; 165 const q2 = v3 * (1 - dx) + v4 * dx; 166 return q1 * (1 - dy) + q2 * dy; 167 }, 168 169 getElevation: function (latlng) { 170 const zoom = this._map.getZoom(); 171 172 let layerPoint = this._map.latLngToLayerPoint(latlng).add(this._map.getPixelOrigin()); 173 if (zoom <= this.options.maxNativeZoom) { 174 return this.getElevationAtLayerPoint(layerPoint); 175 } 176 177 const tileSize = this.getTileSize(); 178 const coordsScale = tileSize.x / this.options.tileSize; 179 layerPoint = layerPoint.subtract([coordsScale / 2, coordsScale / 2]); 180 const elevations = []; 181 for (const [dx, dy] of [ 182 [0, 0], 183 [1, 0], 184 [0, 1], 185 [1, 1], 186 ]) { 187 const res = this.getElevationAtLayerPoint(layerPoint.add([dx * coordsScale, dy * coordsScale])); 188 if (!res.ready || res.error || res.elevation === null) { 189 return res; 190 } 191 elevations.push(res.elevation); 192 } 193 const dx = (mod(layerPoint.x, tileSize.x) / coordsScale) % 1; 194 const dy = (mod(layerPoint.y, tileSize.y) / coordsScale) % 1; 195 return { 196 ready: true, 197 elevation: Math.round(this.bilinearInterpolate(elevations, dx, dy)), 198 }; 199 }, 200 201 onMouseMove: function (e) { 202 this._mousePos = this._map.latLngToLayerPoint(e.latlng); 203 this.label.setLatLng(e.latlng); 204 this.label.addTo(this._map); 205 this.updateElevationDisplay(e.latlng); 206 }, 207 208 onMouseOut: function () { 209 this._map.removeLayer(this.label); 210 }, 211 212 onZoomEnd: function () { 213 if (this._mousePos) { 214 const latlng = this._map.layerPointToLatLng(this._mousePos); 215 this.label.setLatLng(latlng); 216 this.updateElevationDisplay(latlng); 217 } 218 }, 219 220 updateElevationDisplay: function (latlng) { 221 setTimeout(() => this.label.setContent(this.getElevationText(latlng)), 0); 222 }, 223 224 getElevationText: function (latlng) { 225 const elevationResult = this.getElevation(latlng); 226 if (!elevationResult.ready) { 227 return 'Loading...'; 228 } 229 if (elevationResult.error) { 230 return 'Error'; 231 } 232 if (elevationResult.elevation === null) { 233 return 'No data'; 234 } 235 return elevationResult.elevation.toString(); 236 }, 237 }); 238 239 export {ElevationLayer};