nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

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};