├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── NonTiledLayer.ts ├── token.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["dist"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "extends": [ 12 | "airbnb-base", 13 | "airbnb-typescript/base" 14 | ], 15 | "rules": { 16 | "no-param-reassign": "off", 17 | "no-restricted-syntax": "off", 18 | "no-underscore-dangle": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | open-pull-requests-limit: 999 6 | rebase-strategy: disabled 7 | schedule: 8 | interval: weekly 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | setup: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up Node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 17 18 | check-latest: true 19 | cache: npm 20 | 21 | - name: Cache Node modules 22 | id: cache-node-modules 23 | uses: actions/cache@v2 24 | with: 25 | path: node_modules 26 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 27 | 28 | - name: Install dependencies 29 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 30 | run: npm ci 31 | 32 | - name: Cache setup 33 | uses: actions/cache@v2 34 | with: 35 | path: ./* 36 | key: ${{ github.sha }} 37 | 38 | run: 39 | needs: setup 40 | runs-on: ubuntu-latest 41 | strategy: 42 | matrix: 43 | command: [lint, build] 44 | steps: 45 | - name: Restore setup 46 | uses: actions/cache@v2 47 | with: 48 | path: ./* 49 | key: ${{ github.sha }} 50 | 51 | - name: Set up Node 52 | uses: actions/setup-node@v2 53 | with: 54 | node-version: 17 55 | 56 | - name: Run ${{ matrix.command }} task 57 | run: npm run ${{ matrix.command }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-17, PTV Group 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet.NonTiledLayer 2 | 3 | [![Build status](https://github.com/ptv-logistics/Leaflet.NonTiledLayer/workflows/CI/badge.svg)](https://github.com/ptv-logistics/Leaflet.NonTiledLayer/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/leaflet.nontiledlayer.svg)](https://www.npmjs.com/package/leaflet.nontiledlayer) 5 | ![Leaflet compatible!](https://img.shields.io/badge/Leaflet-1.7.x-blue.svg?style=flat) 6 | 7 | ## Purpose 8 | 9 | While Leaflet handles the de-facto standard for stitching a map from tiles very well, 10 | there is no concept for imagery data that cannot be queried in tiles. 11 | 12 | Not all imagery providers can handle tiles properly, for example if they render labels dynamically. 13 | So we've added a Leaflet.NonTiledLayer, which gets the imagery for the complete map viewport whenever it changes. 14 | Leaflet.NonTiledLayer.WMS is the implementation that makes WMS requests, similar to the TileLayer.WMS. 15 | 16 | You can see a demo here: 17 | 18 | https://ptv-logistics.github.io/Leaflet.NonTiledLayer/index.html 19 | 20 | It uses the WMS service of [PTV xServer internet](https://www.ptvgroup.com/en/solutions/products/ptv-xserver/), which requires a tiled/non-tiled hybrid approach (and that is the reason we've built this). 21 | The sample also displays some 3rd-party WMS overlays that also cannot be requested in tiles. 22 | 23 | The layer supports Leaflet 1.7.x. 24 | 25 | ## Installation 26 | 27 | Install using [`npm`](https://www.npmjs.com/package/leaflet.nontiledlayer): 28 | 29 | ```bash 30 | npm install leaflet.nontiledlayer 31 | ``` 32 | 33 | Or [`yarn`](https://yarnpkg.com/en/package/leaflet.nontiledlayer): 34 | 35 | ```bash 36 | yarn add leaflet.nontiledlayer 37 | ``` 38 | 39 | Or use the latest build at https://unpkg.com/leaflet.nontiledlayer/dist/ 40 | 41 | ## The supported options 42 | 43 | * *attribution* - the attribution text for the layer data. Default: ```''``` 44 | * *opacity* - the opacity value between 0.0 and 1.0. Default: ```1.0``` 45 | * *minZoom* - the minimum zoom level for which the overlay is requested. Default: ```0``` 46 | * *maxZoom* - the maximum zoom level for which the overlay is requested. Default: ```18``` 47 | * *bounds* - the geographic bounds of the layer. Default: ```L.latLngBounds([-85.05, -180], [85.05, 180])``` 48 | * *zIndex* - z-index of the overlay. Default: ```undefined``` 49 | * *pane* - the name of the pane where the child div is inserted. Default: ```'overlayPane'``` 50 | * *pointerEvents* - the pointer-events style for the overlay. Default: ```null``` 51 | * *errorImageUrl* - the url of the image displayed when the layer fails to load (invalid request or server error). Default: 1px transparent gif ```data:image/gif;base64,R0lGODlhAQABAHAAACH5BAUAAAAALAAAAAABAAEAAAICRAEAOw==``` 52 | * *useCanvas* - use the canvas to render the images, fixes flickering issues with Firefox, doesn't work on IE8. Setting it to ```undefined``` will use canvas, if available. Default: ```undefined``` 53 | * *detectRetina* - doubles the actual image size requested, if the Browser is in retina mode. Default: ```false``` 54 | * *crossOrigin* - enables cross origin capabilities. Valid values are 'anonymous' and 'use-credentials'. Default: ```undefined``` 55 | 56 | The pane and zIndex properties allow to fine-tune the layer ordering. For example, it is possible to insert a NonTiledLayer between two layers of the tilePane, like the labels [here](https://sharpmapwidgets.azurewebsites.net/), or on top of the vector shapes, like the labels [here](https://ptv-logistics.github.io/fl-labs/) or [here](https://api-eu-test.cloud.ptvgroup.com/samplebrowser/#samples/data-rendering-geoJson/view). 57 | 58 | You can build your own NonTiledLayer by inheriting from NonTiledLayer and implementing either the function getImageUrl or getImageUrlAsync. The getImageUrl just returns an uri and is used by the WMS implementation. The getImageUrlAsync can be used for services that not only return images, but also additional context information for interaction. The project [here](https://ptv-logistics.github.io/Leaflet.PtvLayer/) uses this method. 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet NonTiledLayer Example 6 | 7 | 8 | 9 | 21 | 22 | 23 |
24 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet.nontiledlayer", 3 | "version": "1.0.9", 4 | "description": "A leaflet layer for non-tiled overlays", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/NonTiledLayer-src.js", 9 | "directories": { 10 | "dist": "dist" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/ptv-logistics/Leaflet.NonTiledLayer.git" 15 | }, 16 | "scripts": { 17 | "prepublishOnly": "npm run build", 18 | "build": "rimraf dist && rollup --config", 19 | "lint": "eslint ." 20 | }, 21 | "keywords": [ 22 | "leaflet", 23 | "xserver", 24 | "wms" 25 | ], 26 | "author": { 27 | "name": "Oliver Heilig", 28 | "email": "oliver.heilig@ptvgroup.com" 29 | }, 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/ptv-logistics/Leaflet.NonTiledLayer/issues" 33 | }, 34 | "homepage": "https://github.com/ptv-logistics/Leaflet.NonTiledLayer", 35 | "peerDependencies": { 36 | "leaflet": "^1.7.1" 37 | }, 38 | "devDependencies": { 39 | "@rollup/plugin-commonjs": "^24.0.0", 40 | "@rollup/plugin-node-resolve": "^15.0.0", 41 | "@rollup/plugin-typescript": "^11.0.0", 42 | "@types/leaflet": "^1.7.2", 43 | "@typescript-eslint/eslint-plugin": "^5.0.0", 44 | "@typescript-eslint/parser": "^5.0.0", 45 | "eslint": "^8.0.0", 46 | "eslint-config-airbnb-base": "^15.0.0", 47 | "eslint-config-airbnb-typescript": "^17.0.0", 48 | "rimraf": "^4.0.7", 49 | "rollup": "^2.48.0", 50 | "rollup-plugin-terser": "^7.0.2", 51 | "tslib": "^2.2.0", 52 | "typescript": "^4.3.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | /** @type {import('rollup').OutputOptions} */ 7 | const outputConfig = { 8 | name: 'L.NonTiledLayer', 9 | format: 'umd', 10 | globals: { 11 | leaflet: 'L', 12 | }, 13 | }; 14 | 15 | /** @type {import('rollup').RollupOptions} */ 16 | const config = { 17 | external: ['leaflet'], 18 | input: 'src/NonTiledLayer.ts', 19 | plugins: [ 20 | commonjs(), 21 | nodeResolve(), 22 | typescript(), 23 | ], 24 | output: [ 25 | { 26 | ...outputConfig, 27 | file: 'dist/NonTiledLayer-src.js', 28 | }, 29 | { 30 | ...outputConfig, 31 | file: 'dist/NonTiledLayer.js', 32 | sourcemap: true, 33 | plugins: [terser()], 34 | }, 35 | ], 36 | }; 37 | 38 | export default config; 39 | -------------------------------------------------------------------------------- /src/NonTiledLayer.ts: -------------------------------------------------------------------------------- 1 | import type { LayerOptions } from 'leaflet'; 2 | import { 3 | bind, 4 | Bounds, 5 | Browser, 6 | CRS, 7 | DomUtil, 8 | extend, 9 | LatLng, 10 | LatLngBounds, 11 | Layer, 12 | setOptions, 13 | Util, 14 | } from 'leaflet'; 15 | 16 | export interface NonTiledLayerOptions extends LayerOptions { 17 | /** The opacity value between 0.0 and 1.0. Default: `1.0` */ 18 | opacity?: number; 19 | /** The minimum zoom level for which the overlay is requested. Default: `0` */ 20 | minZoom?: number; 21 | /** The maximum zoom level for which the overlay is requested. Default: `18` */ 22 | maxZoom?: number; 23 | /** The geographic bounds of the layer. Default: `LatLngBounds([-85.05, -180], [85.05, 180])` */ 24 | bounds?: LatLngBounds 25 | /** z-index of the overlay. Default: `undefined` */ 26 | zIndex?: number 27 | /** The pointer-events style for the overlay. Default: `undefined` */ 28 | pointerEvents?: string; 29 | /** 30 | * The url of the image displayed when the layer fails to load (invalid request or server error). 31 | * Default: 1px transparent gif 32 | */ 33 | errorImageUrl?: string; 34 | /** 35 | * Use the canvas to render the images, fixes flickering issues with Firefox, doesn't work on IE8. 36 | * Setting it to `undefined` will use canvas, if available. Default: `undefined` 37 | */ 38 | useCanvas?: boolean; 39 | /** 40 | * Doubles the actual image size requested, if the browser is in retina mode. Default: `false` 41 | */ 42 | detectRetina?: boolean; 43 | /** 44 | * Enables cross origin capabilities. Valid values are 'anonymous' and 'use-credentials'. 45 | * Default: `undefined` 46 | */ 47 | crossOrigin?: 'anonymous' | 'use-credentials'; 48 | } 49 | 50 | /* 51 | * L.NonTiledLayer is an addon for leaflet which renders dynamic image overlays 52 | */ 53 | const NonTiledLayer = Layer.extend({ 54 | emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAHAAACH5BAUAAAAALAAAAAABAAEAAAICRAEAOw==', // 1px transparent GIF 55 | 56 | options: { 57 | attribution: '', 58 | opacity: 1.0, 59 | zIndex: undefined, 60 | minZoom: 0, 61 | maxZoom: 18, 62 | pointerEvents: undefined, 63 | errorImageUrl: 'data:image/gif;base64,R0lGODlhAQABAHAAACH5BAUAAAAALAAAAAABAAEAAAICRAEAOw==', // 1px transparent GIF 64 | bounds: new LatLngBounds([-85.05, -180], [85.05, 180]), 65 | useCanvas: undefined, 66 | detectRetina: false, 67 | }, 68 | 69 | key: '', 70 | 71 | // override this method in the inherited class 72 | // getImageUrl: function (bounds, width, height) {}, 73 | // getImageUrlAsync: function (bounds, width, height, f) {}, 74 | 75 | initialize: function initialize(options: NonTiledLayerOptions) { 76 | setOptions(this, options); 77 | }, 78 | 79 | onAdd: function onAdd(map) { 80 | this._map = map; 81 | 82 | if (!this._div) { 83 | this._div = DomUtil.create('div', 'leaflet-image-layer'); 84 | if (this.options.pointerEvents) { 85 | this._div.style['pointer-events'] = this.options.pointerEvents; 86 | } 87 | if (typeof this.options.zIndex !== 'undefined') { 88 | this._div.style.zIndex = this.options.zIndex; 89 | } 90 | if (typeof this.options.opacity !== 'undefined') { 91 | this._div.style.opacity = this.options.opacity; 92 | } 93 | } 94 | 95 | this.getPane().appendChild(this._div); 96 | 97 | const canvasSupported = !!window.HTMLCanvasElement; 98 | 99 | if (typeof this.options.useCanvas === 'undefined') { 100 | this._useCanvas = canvasSupported; 101 | } else { 102 | this._useCanvas = this.options.useCanvas; 103 | } 104 | 105 | if (this._useCanvas) { 106 | this._bufferCanvas = this._initCanvas(); 107 | this._currentCanvas = this._initCanvas(); 108 | } else { 109 | this._bufferImage = this._initImage(); 110 | this._currentImage = this._initImage(); 111 | } 112 | 113 | this._update(); 114 | }, 115 | 116 | onRemove: function onRemove() { 117 | this.getPane().removeChild(this._div); 118 | 119 | if (this._useCanvas) { 120 | this._div.removeChild(this._bufferCanvas); 121 | this._div.removeChild(this._currentCanvas); 122 | } else { 123 | this._div.removeChild(this._bufferImage); 124 | this._div.removeChild(this._currentImage); 125 | } 126 | }, 127 | 128 | addTo: function addTo(map) { 129 | map.addLayer(this); 130 | return this; 131 | }, 132 | 133 | _setZoom: function setZoom() { 134 | if (this._useCanvas) { 135 | if (this._currentCanvas._bounds) this._resetImageScale(this._currentCanvas, true); 136 | if (this._bufferCanvas._bounds) this._resetImageScale(this._bufferCanvas); 137 | } else { 138 | if (this._currentImage._bounds) this._resetImageScale(this._currentImage, true); 139 | if (this._bufferImage._bounds) this._resetImageScale(this._bufferImage); 140 | } 141 | }, 142 | 143 | getEvents: function getEvents() { 144 | const events: any = { 145 | moveend: this._update, 146 | }; 147 | 148 | if (this._zoomAnimated) { 149 | events.zoomanim = this._animateZoom; 150 | } 151 | 152 | events.zoom = this._setZoom; 153 | 154 | return events; 155 | }, 156 | 157 | getElement: function getElement() { 158 | return this._div; 159 | }, 160 | 161 | setOpacity: function setOpacity(opacity) { 162 | this.options.opacity = opacity; 163 | if (this._div) { 164 | DomUtil.setOpacity(this._div, this.options.opacity); 165 | } 166 | return this; 167 | }, 168 | 169 | setZIndex: function setZIndex(zIndex) { 170 | if (zIndex) { 171 | this.options.zIndex = zIndex; 172 | if (this._div) { 173 | this._div.style.zIndex = zIndex; 174 | } 175 | } 176 | return this; 177 | }, 178 | 179 | // TODO remove bringToFront/bringToBack duplication from TileLayer/Path 180 | bringToFront: function bringToFront() { 181 | if (this._div) { 182 | this.getPane().appendChild(this._div); 183 | } 184 | return this; 185 | }, 186 | 187 | bringToBack: function bringToBack() { 188 | if (this._div) { 189 | this.getPane().insertBefore(this._div, this.getPane().firstChild); 190 | } 191 | return this; 192 | }, 193 | 194 | getAttribution: function getAttribution() { 195 | return this.options.attribution; 196 | }, 197 | 198 | _initCanvas: function initCanvas() { 199 | const canvas = DomUtil.create('canvas', 'leaflet-image-layer') as HTMLCanvasElement & { _image: HTMLImageElement }; 200 | 201 | this._div.appendChild(canvas); 202 | canvas._image = new Image(); 203 | this._ctx = canvas.getContext('2d'); 204 | 205 | if (this.options.crossOrigin) { 206 | canvas._image.crossOrigin = this.options.crossOrigin; 207 | } 208 | 209 | if (this._map.options.zoomAnimation && Browser.any3d) { 210 | DomUtil.addClass(canvas, 'leaflet-zoom-animated'); 211 | } else { 212 | DomUtil.addClass(canvas, 'leaflet-zoom-hide'); 213 | } 214 | 215 | extend(canvas._image, { 216 | onload: bind(this._onImageLoad, this), 217 | onerror: bind(this._onImageError, this), 218 | }); 219 | 220 | return canvas; 221 | }, 222 | 223 | _initImage: function initImage() { 224 | const image = DomUtil.create('img', 'leaflet-image-layer'); 225 | 226 | if (this.options.crossOrigin) { 227 | image.crossOrigin = this.options.crossOrigin; 228 | } 229 | 230 | this._div.appendChild(image); 231 | 232 | if (this._map.options.zoomAnimation && Browser.any3d) { 233 | DomUtil.addClass(image, 'leaflet-zoom-animated'); 234 | } else { 235 | DomUtil.addClass(image, 'leaflet-zoom-hide'); 236 | } 237 | 238 | // TODO createImage util method to remove duplication 239 | extend(image, { 240 | galleryimg: 'no', 241 | onselectstart: Util.falseFn, 242 | onmousemove: Util.falseFn, 243 | onload: bind(this._onImageLoad, this), 244 | onerror: bind(this._onImageError, this), 245 | }); 246 | 247 | return image; 248 | }, 249 | 250 | redraw: function redraw() { 251 | if (this._map) { 252 | this._update(); 253 | } 254 | return this; 255 | }, 256 | 257 | _animateZoom: function animateZoom(e) { 258 | if (this._useCanvas) { 259 | if (this._currentCanvas._bounds) this._animateImage(this._currentCanvas, e); 260 | if (this._bufferCanvas._bounds) this._animateImage(this._bufferCanvas, e); 261 | } else { 262 | if (this._currentImage._bounds) this._animateImage(this._currentImage, e); 263 | if (this._bufferImage._bounds) this._animateImage(this._bufferImage, e); 264 | } 265 | }, 266 | 267 | _animateImage: function animateImage(image, e) { 268 | const map = this._map; 269 | const scale = image._scale * image._sscale * map.getZoomScale(e.zoom); 270 | const nw = image._bounds.getNorthWest(); 271 | const topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center); 272 | 273 | DomUtil.setTransform(image, topLeft, scale); 274 | 275 | image._lastScale = scale; 276 | }, 277 | 278 | _resetImageScale: function resetImageScale(image) { 279 | const bounds = new Bounds( 280 | this._map.latLngToLayerPoint(image._bounds.getNorthWest()), 281 | this._map.latLngToLayerPoint(image._bounds.getSouthEast()), 282 | ); 283 | const orgSize = image._orgBounds.getSize().y; 284 | const scaledSize = bounds.getSize().y; 285 | 286 | const scale = scaledSize / orgSize; 287 | image._sscale = scale; 288 | 289 | DomUtil.setTransform(image, bounds.min, scale); 290 | }, 291 | 292 | _resetImage: function resetImage(image) { 293 | const bounds = new Bounds( 294 | this._map.latLngToLayerPoint(image._bounds.getNorthWest()), 295 | this._map.latLngToLayerPoint(image._bounds.getSouthEast()), 296 | ); 297 | const size = bounds.getSize(); 298 | 299 | DomUtil.setPosition(image, bounds.min); 300 | 301 | image._orgBounds = bounds; 302 | image._sscale = 1; 303 | 304 | if (this._useCanvas) { 305 | image.width = size.x; 306 | image.height = size.y; 307 | } else { 308 | image.style.width = `${size.x}px`; 309 | image.style.height = `${size.y}px`; 310 | } 311 | }, 312 | 313 | _getClippedBounds: function getClippedBounds() { 314 | const wgsBounds = this._map.getBounds(); 315 | 316 | // truncate bounds to valid wgs bounds 317 | let mSouth = wgsBounds.getSouth(); 318 | let mNorth = wgsBounds.getNorth(); 319 | let mWest = wgsBounds.getWest(); 320 | let mEast = wgsBounds.getEast(); 321 | 322 | const lSouth = this.options.bounds.getSouth(); 323 | const lNorth = this.options.bounds.getNorth(); 324 | const lWest = this.options.bounds.getWest(); 325 | const lEast = this.options.bounds.getEast(); 326 | 327 | // mWest = (mWest + 180) % 360 - 180; 328 | if (mSouth < lSouth) mSouth = lSouth; 329 | if (mNorth > lNorth) mNorth = lNorth; 330 | if (mWest < lWest) mWest = lWest; 331 | if (mEast > lEast) mEast = lEast; 332 | 333 | const world1 = new LatLng(mNorth, mWest); 334 | const world2 = new LatLng(mSouth, mEast); 335 | 336 | return new LatLngBounds(world1, world2); 337 | }, 338 | 339 | _getImageScale: function getImageScale() { 340 | return this.options.detectRetina && Browser.retina ? 2 : 1; 341 | }, 342 | 343 | _update: function update() { 344 | const bounds = this._getClippedBounds(); 345 | 346 | // re-project to corresponding pixel bounds 347 | const pix1 = this._map.latLngToContainerPoint(bounds.getNorthWest()); 348 | const pix2 = this._map.latLngToContainerPoint(bounds.getSouthEast()); 349 | 350 | // get pixel size 351 | let width = pix2.x - pix1.x; 352 | let height = pix2.y - pix1.y; 353 | 354 | let i; 355 | if (this._useCanvas) { 356 | // set scales for zoom animation 357 | this._bufferCanvas._scale = this._bufferCanvas._lastScale; 358 | this._currentCanvas._scale = 1; 359 | this._currentCanvas._lastScale = this._currentCanvas._scale; 360 | this._bufferCanvas._sscale = 1; 361 | 362 | this._currentCanvas._bounds = bounds; 363 | 364 | this._resetImage(this._currentCanvas); 365 | 366 | i = this._currentCanvas._image; 367 | 368 | DomUtil.setOpacity(i, 0); 369 | } else { 370 | // set scales for zoom animation 371 | this._bufferImage._scale = this._bufferImage._lastScale; 372 | this._currentImage._scale = 1; 373 | this._currentImage._lastScale = this._currentImage._scale; 374 | this._bufferImage._sscale = 1; 375 | 376 | this._currentImage._bounds = bounds; 377 | 378 | this._resetImage(this._currentImage); 379 | 380 | i = this._currentImage; 381 | 382 | DomUtil.setOpacity(i, 0); 383 | } 384 | 385 | if ( 386 | this._map.getZoom() < this.options.minZoom 387 | || this._map.getZoom() > this.options.maxZoom 388 | || width < 32 389 | || height < 32 390 | ) { 391 | this._div.style.visibility = 'hidden'; 392 | i.src = this.emptyImageUrl; 393 | i.key = ''; 394 | this.key = i.key; 395 | i.tag = null; 396 | return; 397 | } 398 | 399 | // fire loading event 400 | this.fire('loading'); 401 | 402 | width *= this._getImageScale(); 403 | height *= this._getImageScale(); 404 | 405 | // create a key identifying the current request 406 | this.key = [bounds.getNorthWest(), bounds.getSouthEast(), width, height].join(', '); 407 | 408 | if (this.getImageUrl) { 409 | i.src = this.getImageUrl(bounds, width, height); 410 | i.key = this.key; 411 | } else { 412 | this.getImageUrlAsync(bounds, width, height, this.key, (key, url, tag) => { 413 | i.key = key; 414 | i.src = url; 415 | i.tag = tag; 416 | }); 417 | } 418 | }, 419 | 420 | _onImageError: function onImageError(e) { 421 | this.fire('error', e); 422 | DomUtil.addClass(e.target, 'invalid'); 423 | // prevent error loop if error image is not valid 424 | if (e.target.src !== this.options.errorImageUrl) { 425 | e.target.src = this.options.errorImageUrl; 426 | } 427 | }, 428 | 429 | _onImageLoad: function onImageLoad(e) { 430 | if (e.target.src !== this.options.errorImageUrl) { 431 | DomUtil.removeClass(e.target, 'invalid'); 432 | if (!e.target.key || e.target.key !== this.key) { // obsolete / outdated image 433 | return; 434 | } 435 | } 436 | this._onImageDone(e); 437 | 438 | this.fire('load', e); 439 | }, 440 | 441 | _onImageDone: function onImageDone(e) { 442 | let tmp; 443 | 444 | if (this._useCanvas) { 445 | this._renderCanvas(e); 446 | } else { 447 | DomUtil.setOpacity(this._currentImage, 1); 448 | DomUtil.setOpacity(this._bufferImage, 0); 449 | 450 | if (this._addInteraction && this._currentImage.tag) { 451 | this._addInteraction(this._currentImage.tag); 452 | } 453 | 454 | tmp = this._bufferImage; 455 | this._bufferImage = this._currentImage; 456 | this._currentImage = tmp; 457 | } 458 | 459 | if (e.target.key !== '') { 460 | this._div.style.visibility = 'visible'; 461 | } 462 | }, 463 | 464 | _renderCanvas: function renderCanvas() { 465 | const ctx = this._currentCanvas.getContext('2d'); 466 | 467 | ctx.drawImage( 468 | this._currentCanvas._image, 469 | 0, 470 | 0, 471 | this._currentCanvas.width, 472 | this._currentCanvas.height, 473 | ); 474 | 475 | DomUtil.setOpacity(this._currentCanvas, 1); 476 | DomUtil.setOpacity(this._bufferCanvas, 0); 477 | 478 | if (this._addInteraction && this._currentCanvas._image.tag) { 479 | this._addInteraction(this._currentCanvas._image.tag); 480 | } 481 | 482 | const tmp = this._bufferCanvas; 483 | this._bufferCanvas = this._currentCanvas; 484 | this._currentCanvas = tmp; 485 | }, 486 | 487 | }); 488 | 489 | /* 490 | * L.NonTiledLayer.WMS is used for putting WMS non tiled layers on the map. 491 | */ 492 | (NonTiledLayer as any).WMS = NonTiledLayer.extend({ 493 | 494 | defaultWmsParams: { 495 | service: 'WMS', 496 | request: 'GetMap', 497 | version: '1.1.1', 498 | layers: '', 499 | styles: '', 500 | format: 'image/jpeg', 501 | transparent: false, 502 | }, 503 | 504 | options: { 505 | crs: null, 506 | uppercase: false, 507 | }, 508 | 509 | initialize: function initialize(url, options) { // (String, Object) 510 | let i; 511 | 512 | this._wmsUrl = url; 513 | 514 | const wmsParams = extend({}, this.defaultWmsParams); 515 | 516 | // all keys that are not NonTiledLayer options go to WMS params 517 | for (i in options) { 518 | if ( 519 | !Object.prototype.hasOwnProperty.call(NonTiledLayer.prototype.options, i) 520 | && !(Layer && Object.prototype.hasOwnProperty.call((Layer.prototype as any).options, i)) 521 | ) { 522 | wmsParams[i] = options[i]; 523 | } 524 | } 525 | 526 | this.wmsParams = wmsParams; 527 | 528 | setOptions(this, options); 529 | }, 530 | 531 | onAdd: function onAdd(map) { 532 | this._crs = this.options.crs || map.options.crs; 533 | this._wmsVersion = parseFloat(this.wmsParams.version); 534 | 535 | const projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs'; 536 | this.wmsParams[projectionKey] = this._crs.code; 537 | 538 | NonTiledLayer.prototype.onAdd.call(this, map); 539 | }, 540 | 541 | getImageUrl: function getImageUrl(bounds, width, height) { 542 | const { wmsParams } = this; 543 | 544 | wmsParams.width = width; 545 | wmsParams.height = height; 546 | 547 | const nw = this._crs.project(bounds.getNorthWest()); 548 | const se = this._crs.project(bounds.getSouthEast()); 549 | const url = this._wmsUrl; 550 | const bbox = (this._wmsVersion >= 1.3 && this._crs === CRS.EPSG4326 551 | ? [se.y, nw.x, nw.y, se.x] 552 | : [nw.x, se.y, se.x, nw.y]).join(','); 553 | 554 | return url + Util.getParamString(this.wmsParams, url, this.options.uppercase) + (this.options.uppercase ? '&BBOX=' : '&bbox=') + bbox; 555 | }, 556 | 557 | setParams: function setParams(params, noRedraw) { 558 | extend(this.wmsParams, params); 559 | 560 | if (!noRedraw) { 561 | this.redraw(); 562 | } 563 | 564 | return this; 565 | }, 566 | }); 567 | 568 | export default NonTiledLayer; 569 | -------------------------------------------------------------------------------- /token.js: -------------------------------------------------------------------------------- 1 | // add your xserver-internet token here 2 | const token = ''; 3 | 4 | export default token; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true 7 | } 8 | } --------------------------------------------------------------------------------