├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── bound.js ├── circle.js ├── helper │ ├── asyncQueue.js │ └── geo.js ├── image.js ├── marker.js ├── multipolygon.js ├── polyline.js ├── staticmaps.js ├── text.js └── tileserverconfig.js └── test ├── marker.png ├── out └── .gitignore ├── static ├── geojson.js ├── multipolygonGeometry.js ├── route.js └── routeLong.js ├── staticmaps.js └── unit └── circle.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-transform-runtime", { 4 | "helpers": false, 5 | "regenerator": true 6 | }] 7 | ], 8 | "presets": [ 9 | "@babel/preset-env" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ], 6 | "parser": "@babel/eslint-parser", 7 | "parserOptions": { 8 | "ecmaVersion": 2018, 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true 11 | } 12 | }, 13 | "globals": { 14 | "document": true, 15 | "window": true, 16 | "location": true 17 | }, 18 | "rules": { 19 | "no-underscore-dangle": 0, 20 | "no-plusplus": 0, 21 | "no-console": 0 22 | }, 23 | "env": { 24 | "node": true, 25 | "mocha": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | test/out/* 5 | dist 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | README.md 4 | .eslintrc 5 | .babelrc 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Stephan Georg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StaticMaps [![npm version](https://badge.fury.io/js/staticmaps.svg)](https://badge.fury.io/js/staticmaps) 2 | A Node.js library for creating map images with markers, polylines, polygons and text. This library is a JavaScript implementation of [Static Map](https://github.com/komoot/staticmap). 3 | 4 | ![Map with polyline](https://stephangeorg.github.io/staticmaps/sample/polyline.png?raw=true=800x280) 5 | 6 | ## Prerequisites 7 | 8 | Image manipulation is based on **[Sharp](https://sharp.pixelplumbing.com/)**. Pre-compiled [libvips](https://github.com/libvips/libvips) binaries for sharp are provided for use with Node.js versions 14+ on macOS (x64, ARM64), Linux (x64, ARM64) and Windows (x64, x86) platforms. For other OS or using with **Heroku, Docker, AWS Lambda** please refer to [sharp installation instructions](https://sharp.pixelplumbing.com/install). 9 | 10 | ## Releases 11 | 12 | Version | sharp | libvips | Node.js (pre-compiled) 13 | ----------------- | ---------------- | ------- | ------------- 14 | 1.13.1+ | 0.33.2 | 8.15.1 | 18.17.0+ 15 | 1.12.0 | 0.31.3 | 8.13.3 | 14.15.0+ 16 | 1.11.1 | 0.31.3 | 8.13.3 | 14.15.0+ 17 | 1.10.0 | 0.30.7 | 8.12.2 | 12.13.0+ 18 | 19 | [Changelog](https://github.com/StephanGeorg/staticmaps/releases) 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npm i staticmaps 25 | ``` 26 | 27 | ## Getting Started 28 | 29 | ### Initialization ### 30 | ```javascript 31 | import StaticMaps from 'staticmaps'; 32 | ``` 33 | ```javascript 34 | const options = { 35 | width: 600, 36 | height: 400 37 | }; 38 | const map = new StaticMaps(options); 39 | ``` 40 | #### Map options 41 | 42 | Parameter | Default | Description 43 | ------------------- | ------------------- | ------------- 44 | width | Required | Width of the output image in px 45 | height | Required | Height of the output image in px 46 | paddingX | 0 | (optional) Minimum distance in px between map features and map border 47 | paddingY | 0 | (optional) Minimum distance in px between map features and map border 48 | tileUrl | | (optional) Tile server URL for the map base layer or `null` for empty base layer. `{x},{y},{z}` or `{quadkey}` supported. 49 | tileSubdomains | [] | (optional) Subdomains of tile server, usage `['a', 'b', 'c']` 50 | tileLayers | [] | (optional) Tile layers to use, usage `[{tileUrl: ..., tileSubdomains: ...}, {tileUrl: ..., tileSubdomains: ...}]` (replaces `tileUrl` and `tileSubdomains` if set) 51 | tileSize | 256 | (optional) Tile size in pixel 52 | tileRequestTimeout | | (optional) Timeout for the tiles request 53 | tileRequestHeader | {} | (optional) Additional headers for the tiles request (default: {}) 54 | tileRequestLimit | 2 | (optional) Limit concurrent connections to the tiles server 55 | zoomRange | { min: 1, max: 17 } | (optional) Defines the range of zoom levels to try 56 | maxZoom | | (optional) DEPRECATED: Use zoomRange.max instead: forces zoom to stay at least this far from the surface, useful for tile servers that error on high levels 57 | reverseY | false | (optional) If true, reverse the y index of the tiles to match the TMS naming format 58 | 59 | ### Methods 60 | 61 | Method | Description 62 | ------------------- | ------------- 63 | [addMarker](#addmarker-options) | Adds a marker to the map 64 | [addLine](#addline-options) | Adds a polyline to the map 65 | [addPolygon](#addpolygon-options) | Adds a polygon to the map 66 | [addMultiPolygon](#addmultipolygon-options) | Adds a multipolygon to the map 67 | [addCircle](#addcircle-options) | Adds a circle to the map 68 | [addText](#addtext-options) | Adds text to the map 69 | [render](#render-center-zoom) | Renders the map and added features 70 | [image.save](#imagesave-filename-outputoptions) | Saves the map image to a file 71 | [image.buffer](#imagebuffer-mime-outputoptions) | Saves the map image to a buffer 72 | 73 | #### addMarker (options) 74 | Adds a marker to the map. 75 | ##### Marker options 76 | Parameter | Default | Description 77 | ------------------- | --------- | ------------- 78 | coord | Required | Coordinates of the marker ([Lng, Lat]) 79 | img | Required | Marker image path or URL 80 | height | Required | Height of marker image in px 81 | width | Required | Width of marker image in px 82 | drawHeight | height | (optional) Resize marker image to height in px 83 | drawWidth | width | (optional) Resize marker image to width in px 84 | resizeMode | cover | (optional) Applied resize method if needed. See: [https://sharp.pixelplumbing.com/api-resize] 85 | offsetX | width/2 | (optional) X offset of the marker image in px 86 | offsetY | height | (optional) Y offset of the marker image in px 87 | ##### Usage example 88 | ```javascript 89 | const marker = { 90 | img: `${__dirname}/marker.png`, // can also be a URL 91 | offsetX: 24, 92 | offsetY: 48, 93 | width: 48, 94 | height: 48, 95 | coord : [13.437524,52.4945528] 96 | }; 97 | map.addMarker(marker); 98 | ``` 99 | *** 100 | #### addLine (options) 101 | Adds a polyline to the map. 102 | ##### Polyline options 103 | Parameter | Default | Description 104 | ------------------- | --------- |------------- 105 | coords | Required | Coordinates of the polyline ([[Lng, Lat], ... ,[Lng, Lat]]) 106 | color | #000000BB | (optional) Stroke color of the polyline 107 | width | 3 | (optional) Stroke width of the polyline 108 | ##### Usage example 109 | ```javascript 110 | const polyline = { 111 | coords: [ 112 | [13.399259,52.482659], 113 | [13.387849,52.477144], 114 | [13.40538,52.510632] 115 | ], 116 | color: '#0000FFBB', 117 | width: 3 118 | }; 119 | 120 | map.addLine(polyline); 121 | ``` 122 | *** 123 | 124 | #### addPolygon (options) 125 | Adds a polygon to the map. Polygon is the same as a polyline but first and last coordinate are equal. 126 | ``` 127 | map.addPolygon(options); 128 | ``` 129 | ##### Polygon options 130 | Parameter | Default | Description 131 | ------------------- | --------- | ------------- 132 | coords | Required | Coordinates of the polygon ([[Lng, Lat], ... ,[Lng, Lat]]) 133 | color | #000000BB | (optional) Stroke color of the polygon 134 | width | 3 | (optional) Stroke width of the polygon 135 | fill | #000000BB | (optional) Fill color of the polygon 136 | ##### Usage example 137 | ```javascript 138 | const polygon = { 139 | coords: [ 140 | [13.399259,52.482659], 141 | [13.387849,52.477144], 142 | [13.40538,52.510632], 143 | [13.399259,52.482659] 144 | ], 145 | color: '#0000FFBB', 146 | width: 3 147 | }; 148 | 149 | map.addPolygon(polygon); 150 | ``` 151 | *** 152 | 153 | #### addMultiPolygon (options) 154 | Adds a multipolygon to the map. 155 | ``` 156 | map.addMultiPolygon(options); 157 | ``` 158 | ##### Multipolygon options 159 | Parameter | Default | Description 160 | ------------------- | --------- | ------------- 161 | coords | Required | Coordinates of the multipolygon ([[Lng, Lat], ... ,[Lng, Lat]]) 162 | color | #000000BB | (optional) Stroke color of the multipolygon 163 | width | 3 | (optional) Stroke width of the multipolygon 164 | fill | #000000BB | (optional) Fill color of the multipolygon 165 | ##### Usage example 166 | ```javascript 167 | const multipolygon = { 168 | coords: [ 169 | [ 170 | [-89.9619685, 41.7792032], 171 | [-89.959505, 41.7792084], 172 | [-89.9594928, 41.7827904], 173 | [-89.9631906, 41.7827815], 174 | [-89.9632678, 41.7821559], 175 | [-89.9634801, 41.7805341], 176 | [-89.9635341, 41.780109], 177 | [-89.9635792, 41.7796834], 178 | [-89.9636183, 41.7792165], 179 | [-89.9619685, 41.7792032], 180 | ], 181 | [ 182 | [-89.9631647, 41.7809413], 183 | [-89.9632927, 41.7809487], 184 | [-89.9631565, 41.781985], 185 | [-89.9622404, 41.7819137], 186 | [-89.9623616, 41.780997], 187 | [-89.963029, 41.7810114], 188 | [-89.9631647, 41.7809413], 189 | ], 190 | ], 191 | color: '#0000FFBB', 192 | width: 3 193 | }; 194 | 195 | map.addMultiPolygon(multipolygon); 196 | ``` 197 | *** 198 | 199 | #### addCircle (options) 200 | Adds a circle to the map. 201 | ``` 202 | map.addCircle(options); 203 | ``` 204 | ##### Circle options 205 | Parameter | Default | Description 206 | ------------------- | --------- | ------------- 207 | coord | Required | Coordinate of center of circle 208 | radius | Required | Circle radius in meter 209 | color | #000000BB | (optional) Stroke color of the circle 210 | width | 3 | (optional) Stroke width of the circle 211 | fill | #AA0000BB | (optional) Fill color of the circle 212 | ##### Usage example 213 | ```javascript 214 | const circle = { 215 | coord: [13.01, 51.98], 216 | radius: 500, 217 | fill: '#000000', 218 | width: 0, 219 | }; 220 | 221 | map.addCircle(circle); 222 | ``` 223 | *** 224 | 225 | #### addText (options) 226 | Adds text to the map. 227 | ``` 228 | map.addText(options) 229 | ``` 230 | ##### Text options 231 | Parameter | Default | Description 232 | ----------------- | --------- | -------------- 233 | coord | Required | Coordinates of the text ([x, y]) 234 | text | Required | The text to render 235 | color | #000000BB | (optional) Stroke color of the text 236 | width | 1px | (optional) Stroke width of the text 237 | fill | #000000 | (optional) Fill color of the text 238 | size | 12 | (optional) Font-size of the text 239 | font | Arial | (optional) Font-family of the text 240 | anchor | start | (optional) Anchor of the text (`start`, `middle` or `end`) 241 | offsetX | 0 | (optional) X offset of the text in px. 242 | offsetY | 0 | (optional) Y offset of the text in px. 243 | 244 | ##### Usage example 245 | ```javascript 246 | const text = { 247 | coord: [13.437524, 52.4945528], 248 | text: 'My Text', 249 | size: 50, 250 | width: 1, 251 | fill: '#000000', 252 | color: '#ffffff', 253 | font: 'Calibri', 254 | anchor: 'middle' 255 | }; 256 | 257 | map.addText(text); 258 | ``` 259 | 260 | *** 261 | 262 | #### render (center, zoom) 263 | Renders the map. 264 | ``` 265 | map.render(); 266 | ``` 267 | ##### Render options 268 | Parameter | Default | Description 269 | ------------------- | --------- | ------------- 270 | center | | (optional) Set center of map to a specific coordinate ([Lng, Lat]) 271 | zoom | | (optional) Set a specific zoom level. 272 | 273 | *** 274 | 275 | #### image.save (fileName, [outputOptions]) 276 | Saves the image to a file in `fileName`. 277 | ``` 278 | map.image.save('my-staticmap-image.png', { compressionLevel: 9 }); 279 | ``` 280 | ##### Arguments 281 | Parameter | Default | Description 282 | ------------------- | ----------- | ------------- 283 | fileName | output.png | Name of the output file. Specify output format (png, jpg, webp) by adding file extension. 284 | outputOptions | | (optional) Output options set for [sharp](http://sharp.pixelplumbing.com/en/stable/api-output/#png) 285 | 286 | The `outputOptions` replaces the deprectated `quality` option. For Backwards compatibility `quality` still works but will be overwritten with `outputOptions.quality`. 287 | 288 | 289 | ##### Returns 290 | ``` 291 | 292 | ``` 293 | ~~If callback is undefined it return a Promise.~~ DEPRECATED 294 | 295 | *** 296 | 297 | #### image.buffer (mime, [outputOptions]) 298 | Saves the image to a buffer. 299 | ``` 300 | map.image.buffer('image/jpeg', { quality: 75 }); 301 | ``` 302 | ##### Arguments 303 | Parameter | Default | Description 304 | ------------------- | ----------- | ------------- 305 | mime | image/png | Mime type(`image/png`, `image/jpg` or `image/webp`) of the output buffer 306 | outputOptions | {} | (optional) Output options set for [sharp](http://sharp.pixelplumbing.com/en/stable/api-output/#png) 307 | 308 | The `outputOptions` replaces the deprectated `quality` option. For Backwards compatibility `quality` still works but will be overwritten with `outputOptions.quality`. 309 | 310 | ##### Returns 311 | ``` 312 | 313 | ``` 314 | ~~If callback is undefined it return a Promise.~~ DEPRECATED 315 | 316 | ## Usage Examples 317 | 318 | ### Simple map w/ zoom and center 319 | ```javascript 320 | const zoom = 13; 321 | const center = [13.437524,52.4945528]; 322 | 323 | await map.render(center, zoom); 324 | await map.image.save('center.png'); 325 | 326 | ``` 327 | #### Output 328 | ![Map with zoom and center](https://stephangeorg.github.io/staticmaps/sample/center.png) 329 | 330 | ### Simple map with bounding box 331 | 332 | If specifying a bounding box instead of a center, the optimal zoom will be calculated. 333 | 334 | ```javascript 335 | const bbox = [ 336 | 11.414795,51.835778, // lng,lat of first point 337 | 11.645164,51.733833 // lng,lat of second point, ... 338 | ]; 339 | 340 | await map.render(bbox); 341 | await map.image.save('bbox.png'); 342 | 343 | ``` 344 | #### Output 345 | ![Map with bbox](https://stephangeorg.github.io/staticmaps/sample/bbox.png) 346 | 347 | *** 348 | 349 | ### Map with single marker 350 | 351 | ```javascript 352 | const marker = { 353 | img: `${__dirname}/marker.png`, // can also be a URL, 354 | offsetX: 24, 355 | offsetY: 48, 356 | width: 48, 357 | height: 48, 358 | coord: [13.437524, 52.4945528], 359 | }; 360 | map.addMarker(marker); 361 | await map.render(); 362 | await map.image.save('single-marker.png'); 363 | 364 | ``` 365 | You're free to specify a center as well, otherwise the marker will be centered. 366 | 367 | #### Output 368 | ![Map with marker](https://stephangeorg.github.io/staticmaps/sample/marker.png) 369 | 370 | *** 371 | 372 | ### Map with multiple marker 373 | ```javascript 374 | const marker = { 375 | img: `${__dirname}/marker.png`, // can also be a URL 376 | offsetX: 24, 377 | offsetY: 48, 378 | width: 48, 379 | height: 48 380 | }; 381 | 382 | marker.coord = [13.437524,52.4945528]; 383 | map.addMarker(marker); 384 | marker.coord = [13.430524,52.4995528]; 385 | map.addMarker(marker); 386 | marker.coord = [13.410524,52.5195528]; 387 | map.addMarker(marker); 388 | 389 | await map.render(); 390 | await map.image.save('multiple-marker.png'); 391 | 392 | ``` 393 | #### Output 394 | ![Map with multiple markers](https://stephangeorg.github.io/staticmaps/sample/multiple-marker.png?raw=true) 395 | 396 | *** 397 | 398 | ### Map with polyline 399 | ```javascript 400 | 401 | var line = { 402 | coords: [ 403 | [13.399259,52.482659], 404 | [13.387849,52.477144], 405 | [13.40538,52.510632] 406 | ], 407 | color: '#0000FFBB', 408 | width: 3 409 | }; 410 | 411 | map.addLine(line); 412 | await map.render(); 413 | await map.image.save('test/out/polyline.png'); 414 | 415 | ``` 416 | #### Output 417 | ![Map with polyline](https://stephangeorg.github.io/staticmaps/sample/polyline.png?raw=true=800x280) 418 | 419 | *** 420 | 421 | ### Map with circle 422 | ```javascript 423 | 424 | const circle = { 425 | coord: [13.01, 51.98], 426 | radius: 500, 427 | fill: '#000000', 428 | width: 0, 429 | }; 430 | 431 | map.addCircle(circle); 432 | await map.render(); 433 | await map.image.save('test/out/099-circle.png'); 434 | 435 | ``` 436 | #### Output 437 | ![Map with circle](https://user-images.githubusercontent.com/7861660/129888175-c2209cca-6ede-43d7-bb8d-181fdd4cfa17.png) 438 | 439 | *** 440 | 441 | ### Blue Marble by NASA with text 442 | ```javascript 443 | const options = { 444 | width: 1200, 445 | height: 800, 446 | tileUrl: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/BlueMarble_NextGeneration/default/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpg', 447 | zoomRange: { 448 | max: 8, // NASA server does not support level 9 or higher 449 | } 450 | }; 451 | 452 | const map = new StaticMaps(options); 453 | const text = { 454 | coord: [13.437524, 52.4945528], 455 | text: 'My Text', 456 | size: 50, 457 | width: '1px', 458 | fill: '#000000', 459 | color: '#ffffff', 460 | font: 'Calibri' 461 | }; 462 | 463 | map.addText(text); 464 | 465 | await map.render([13.437524, 52.4945528]); 466 | await map.image.save('test/out/bluemarbletext.png'); 467 | ``` 468 | 469 | #### Output 470 | ![NASA Blue Marble with text](https://i.imgur.com/Jb6hsju.jpg) 471 | 472 | *** 473 | 474 | ### Tile server with subdomains 475 | {s} - subdomain (subdomain), is necessary in order not to fall into the limit for requests to the same domain. Some servers can block your IP if you get tiles from one of subdomains of tile server. 476 | ```javascript 477 | const options = { 478 | width: 1024, 479 | height: 1024, 480 | tileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 481 | tileSubdomains: ['a', 'b', 'c'], 482 | }; 483 | 484 | const map = new StaticMaps(options); 485 | 486 | await map.render([13.437524, 52.4945528], 13); 487 | await map.image.save('test/out/subdomains.png'); 488 | ``` 489 | 490 | ### Mulitple tile layers 491 | 492 | 493 | ```javascript 494 | const options = { 495 | width: 1024, 496 | height: 600, 497 | tileLayers: [{ 498 | tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', 499 | }, { 500 | tileUrl: 'http://www.openfiremap.de/hytiles/{z}/{x}/{y}.png', 501 | }], 502 | }; 503 | 504 | const map = new StaticMaps(options); 505 | 506 | await map.render([13.437524, 52.4945528], 13); 507 | await map.image.save('test/out/multipleLayers.png'); 508 | ``` 509 | 510 | #### Output 511 | ![11-layers](https://user-images.githubusercontent.com/7861660/213999766-a6c7d2bc-5c90-4da4-9df7-08bcb08442ce.png) 512 | 513 | 514 | # Contributers 515 | 516 | + [Stefan Warnat](https://github.com/swarnat) 517 | + [Jordi Casadevall Franco](https://github.com/JOGUI22) 518 | + [Joe Beuckman](https://github.com/jbeuckm) 519 | + [Ergashev Adizbek](https://github.com/Adizbek) 520 | + [Olivier Kamers](https://github.com/OlivierKamers) 521 | + [Wesley Flynn](https://github.com/wesflynn) 522 | + [Thomas Konings](https://github.com/tkon99) 523 | + [Gihan S](https://github.com/gihanshp) 524 | + [Sergey Averyanov](https://github.com/saveryanov) 525 | + [boxcc](https://github.com/boxcc) 526 | + [Maksim Skutin](https://github.com/mskutin) 527 | + [Jorgen Phillips](https://github.com/JorgenPhi) 528 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "staticmaps", 3 | "version": "1.13.1", 4 | "description": "A Node.js library for creating map images with markers, polylines, polygons and text.", 5 | "author": "Stephan Georg ", 6 | "license": "MIT", 7 | "repository": "github:StephanGeorg/staticmaps", 8 | "main": "./dist/staticmaps.js", 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "test": "mocha --require @babel/register", 14 | "build": "babel src --presets @babel/preset-env --plugins @babel/plugin-transform-runtime --out-dir dist", 15 | "prepublish": "npm run build" 16 | }, 17 | "keywords": [ 18 | "openstreetmap", 19 | "osm", 20 | "staticmaps", 21 | "staticmap", 22 | "map", 23 | "maps" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/StephanGeorg/staticmaps/issues" 27 | }, 28 | "homepage": "https://github.com/StephanGeorg/staticmaps#readme", 29 | "dependencies": { 30 | "@babel/runtime": "^7.23.5", 31 | "@turf/bbox": "^6.5.0", 32 | "@turf/circle": "^6.5.0", 33 | "got": "^11.8.6", 34 | "lodash.chunk": "^4.2.0", 35 | "lodash.find": "^4.6.0", 36 | "lodash.first": "^3.0.0", 37 | "lodash.flatten": "^4.4.0", 38 | "lodash.isequal": "^4.5.0", 39 | "lodash.last": "^3.0.0", 40 | "lodash.uniqby": "^4.7.0", 41 | "modern-async": "^1.1.4", 42 | "sharp": "0.33.2" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.23.4", 46 | "@babel/core": "^7.23.5", 47 | "@babel/eslint-parser": "^7.23.3", 48 | "@babel/plugin-transform-runtime": "^7.23.4", 49 | "@babel/preset-env": "^7.23.5", 50 | "@babel/register": "^7.22.15", 51 | "chai": "^4.3.10", 52 | "eslint": "^8.55.0", 53 | "eslint-config-airbnb-base": "^15.0.0", 54 | "eslint-plugin-import": "^2.29.0", 55 | "mocha": "^10.2.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/bound.js: -------------------------------------------------------------------------------- 1 | import bbox from '@turf/bbox'; 2 | 3 | export default class Bound { 4 | constructor(options = {}) { 5 | this.options = options; 6 | this.coords = this.options.coords; 7 | } 8 | 9 | /** 10 | * calculate the coordinates of the envelope / bounding box: (min_lon, min_lat, max_lon, max_lat) 11 | */ 12 | extent() { 13 | const line = { 14 | type: 'LineString', 15 | coordinates: this.coords, 16 | }; 17 | return bbox(line); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/circle.js: -------------------------------------------------------------------------------- 1 | import circle from '@turf/circle'; 2 | import bbox from '@turf/bbox'; 3 | 4 | export default class Circle { 5 | constructor(options = {}) { 6 | this.options = options; 7 | this.coord = this.options.coord; 8 | this.radius = Number(this.options.radius); 9 | this.color = this.options.color || '#000000BB'; 10 | this.fill = this.options.fill || '#AA0000BB'; 11 | this.width = Number.isFinite(this.options.width) ? Number(this.options.width) : 3; 12 | 13 | if (!this.coord || !Array.isArray(this.coord) || this.coord.length < 2) throw new Error('Specify center of circle'); 14 | if (!this.radius || !Number(this.radius)) throw new Error('Specify radius of circle'); 15 | } 16 | 17 | /** 18 | * calculate the coordinates of the envelope / bounding box: (min_lon, min_lat, max_lon, max_lat) 19 | */ 20 | extent() { 21 | const center = { 22 | type: 'Feature', 23 | geometry: { 24 | type: 'Point', 25 | coordinates: this.coord, 26 | }, 27 | }; 28 | const circ = circle(center, (this.radius / 1000), { steps: 100 }); 29 | const circleBbox = bbox(circ); 30 | return circleBbox; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/helper/asyncQueue.js: -------------------------------------------------------------------------------- 1 | const workOnQueue = async (queue, index = 0) => { 2 | if (!queue[index]) return true; // Finished 3 | await queue[index](); 4 | await workOnQueue(queue, (index + 1)); 5 | return false; 6 | }; 7 | 8 | export default workOnQueue; 9 | -------------------------------------------------------------------------------- /src/helper/geo.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * Transform longitude to tile number 4 | */ 5 | lonToX(lon, zoom) { 6 | return ((lon + 180) / 360) * (2 ** zoom); 7 | }, 8 | 9 | /** 10 | * Transform latitude to tile number 11 | */ 12 | latToY(lat, zoom) { 13 | return (1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 14 | / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * (2 ** zoom); 15 | }, 16 | 17 | yToLat(y, zoom) { 18 | return Math.atan(Math.sinh(Math.PI * (1 - 2 * y / (2 ** zoom)))) 19 | / Math.PI * 180; 20 | }, 21 | 22 | xToLon(x, zoom) { 23 | return x / (2 ** zoom) * 360 - 180; 24 | }, 25 | 26 | meterToPixel(meter, zoom, lat) { 27 | const latitudeRadians = lat * (Math.PI / 180); 28 | const meterProPixel = (156543.03392 * Math.cos(latitudeRadians)) / 2 ** zoom; 29 | return meter / meterProPixel; 30 | }, 31 | 32 | tileXYToQuadKey(x, y, z) { 33 | const quadKey = []; 34 | for (let i = z; i > 0; i--) { 35 | let digit = '0'; 36 | const mask = 1 << (i - 1); 37 | if ((x & mask) !== 0) digit++; 38 | if ((y & mask) !== 0) { 39 | digit++; 40 | digit++; 41 | } 42 | quadKey.push(digit); 43 | } 44 | return quadKey.join(''); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/image.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | import last from 'lodash.last'; 3 | 4 | export default class Image { 5 | constructor(options = {}) { 6 | this.options = options; 7 | this.width = this.options.width; 8 | this.height = this.options.height; 9 | this.quality = this.options.quality || 100; 10 | } 11 | 12 | /** 13 | * Prepare all tiles to fit the baselayer 14 | */ 15 | prepareTileParts(data) { 16 | return new Promise((resolve) => { 17 | const tile = sharp(data.body); 18 | tile 19 | .metadata() 20 | .then((metadata) => { 21 | const x = data.box[0]; 22 | const y = data.box[1]; 23 | const sx = x < 0 ? 0 : x; 24 | const sy = y < 0 ? 0 : y; 25 | const dx = x < 0 ? -x : 0; 26 | const dy = y < 0 ? -y : 0; 27 | const extraWidth = x + (metadata.width - this.width); 28 | const extraHeight = y + (metadata.width - this.height); 29 | const w = metadata.width + (x < 0 ? x : 0) - (extraWidth > 0 ? extraWidth : 0); 30 | const h = metadata.height + (y < 0 ? y : 0) - (extraHeight > 0 ? extraHeight : 0); 31 | 32 | // Fixed #20 https://github.com/StephanGeorg/staticmaps/issues/20 33 | if (!w || !h) { 34 | resolve({ success: false }); 35 | return null; 36 | } 37 | 38 | return tile 39 | .extract({ 40 | left: dx, 41 | top: dy, 42 | width: w, 43 | height: h, 44 | }) 45 | .toBuffer() 46 | .then((part) => { 47 | resolve({ 48 | success: true, 49 | position: { top: Math.round(sy), left: Math.round(sx) }, 50 | data: part, 51 | }); 52 | }) 53 | .catch(() => resolve({ success: false })); 54 | }) 55 | .catch(() => resolve({ success: false })); 56 | }); 57 | } 58 | 59 | async draw(tiles) { 60 | this.tempBuffer = null; 61 | if (!this.image) { 62 | // Generate baseimage 63 | const baselayer = sharp({ 64 | create: { 65 | width: this.width, 66 | height: this.height, 67 | channels: 4, 68 | background: { 69 | r: 0, g: 0, b: 0, alpha: 0, 70 | }, 71 | }, 72 | }); 73 | // Save baseImage as buffer 74 | this.tempBuffer = await baselayer.png().toBuffer(); 75 | } else { 76 | this.tempBuffer = this.image; 77 | } 78 | 79 | // Prepare tiles for composing baselayer 80 | const tileParts = []; 81 | tiles.forEach((tile, i) => { 82 | tileParts.push(this.prepareTileParts(tile, i)); 83 | }); 84 | 85 | const preparedTiles = (await Promise.all(tileParts)).filter((v) => v.success); 86 | 87 | // Compose all prepared tiles to the baselayer 88 | const preparedTilesForSharp = preparedTiles 89 | .filter((preparedTile) => !!preparedTile) // remove non-existing tiles 90 | .map((preparedTile) => { 91 | const { position, data } = preparedTile; 92 | position.top = Math.round(position.top); 93 | position.left = Math.round(position.left); 94 | return { input: data, ...position }; 95 | }); 96 | 97 | this.tempBuffer = await sharp(this.tempBuffer) 98 | .composite(preparedTilesForSharp) 99 | .toBuffer(); 100 | 101 | this.image = this.tempBuffer; 102 | return true; 103 | } 104 | 105 | /** 106 | * Save image to file 107 | */ 108 | async save(fileName = 'output.png', outOpts = {}) { 109 | const format = last(fileName.split('.')); 110 | const outputOptions = outOpts; 111 | outputOptions.quality = outputOptions.quality || this.quality; 112 | switch (format.toLowerCase()) { 113 | case 'webp': await sharp(this.image).webp(outputOptions).toFile(fileName); break; 114 | case 'jpg': 115 | case 'jpeg': await sharp(this.image).jpeg(outputOptions).toFile(fileName); break; 116 | case 'png': 117 | default: await sharp(this.image).png(outputOptions).toFile(fileName); 118 | } 119 | } 120 | 121 | /** 122 | * Return image as buffer 123 | */ 124 | async buffer(mime = 'image/png', outOpts = {}) { 125 | const outputOptions = outOpts; 126 | outputOptions.quality = outputOptions.quality || this.quality; 127 | switch (mime.toLowerCase()) { 128 | case 'image/webp': return sharp(this.image).webp(outputOptions).toBuffer(); 129 | case 'image/jpeg': 130 | case 'image/jpg': return sharp(this.image).jpeg(outputOptions).toBuffer(); 131 | case 'image/png': 132 | default: return sharp(this.image).png(outputOptions).toBuffer(); 133 | } 134 | } 135 | } 136 | 137 | module.exports = Image; 138 | -------------------------------------------------------------------------------- /src/marker.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(options = {}) { 3 | this.options = options; 4 | 5 | this.coord = this.options.coord; 6 | this.img = this.options.img; 7 | 8 | this.height = Number.isFinite(this.options.height) 9 | ? Number(this.options.height) : null; 10 | this.width = Number.isFinite(this.options.width) 11 | ? Number(this.options.width) : null; 12 | 13 | this.drawWidth = Number(this.options.drawWidth || this.options.width); 14 | this.drawHeight = Number(this.options.drawHeight || this.options.height); 15 | this.resizeMode = this.options.resizeMode || 'cover'; 16 | 17 | this.offsetX = Number.isFinite(this.options.offsetX) 18 | ? Number(this.options.offsetX) : this.drawWidth / 2; 19 | 20 | this.offsetY = Number.isFinite(this.options.offsetY) 21 | ? Number(this.options.offsetY) : this.drawHeight; 22 | this.offset = [this.offsetX, this.offsetY]; 23 | } 24 | 25 | setSize(width, height) { 26 | this.width = Number(width); 27 | this.height = Number(height); 28 | 29 | if (Number.isNaN(this.drawWidth)) { 30 | this.drawWidth = this.width; 31 | } 32 | 33 | if (Number.isNaN(this.drawHeight)) { 34 | this.drawHeight = this.height; 35 | } 36 | } 37 | 38 | /** 39 | * Set icon data 40 | */ 41 | set(img) { 42 | this.imgData = img; 43 | } 44 | 45 | extentPx() { 46 | return [ 47 | this.offset[0], 48 | (this.height - this.offset[1]), 49 | (this.width - this.offset[0]), 50 | this.offset[1], 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/multipolygon.js: -------------------------------------------------------------------------------- 1 | import flatten from 'lodash.flatten'; 2 | 3 | export default class MultiPolygon { 4 | constructor(options = {}) { 5 | this.options = options; 6 | this.coords = this.options.coords; 7 | this.color = this.options.color || '#000000BB'; 8 | this.fill = this.options.fill; 9 | this.width = Number.isFinite(this.options.width) ? Number(this.options.width) : 3; 10 | this.simplify = this.options.simplify || false; 11 | } 12 | 13 | /** 14 | * calculate the coordinates of the envelope / bounding box: (min_lon, min_lat, max_lon, max_lat) 15 | */ 16 | extent() { 17 | const allPoints = flatten(this.coords); 18 | 19 | return [ 20 | Math.min(...allPoints.map((c) => c[0])), 21 | Math.min(...allPoints.map((c) => c[1])), 22 | Math.max(...allPoints.map((c) => c[0])), 23 | Math.max(...allPoints.map((c) => c[1])), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/polyline.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash.isequal'; 2 | import first from 'lodash.first'; 3 | import last from 'lodash.last'; 4 | 5 | 6 | export default class Polyline { 7 | constructor(options = {}) { 8 | this.options = options; 9 | this.coords = this.options.coords; 10 | this.color = this.options.color || '#000000BB'; 11 | this.fill = this.options.fill; 12 | this.width = Number.isFinite(this.options.width) ? Number(this.options.width) : 3; 13 | this.simplify = this.options.simplify || false; 14 | this.type = (isEqual(first(this.coords), last(this.coords))) 15 | ? 'polygon' : 'polyline'; 16 | } 17 | 18 | /** 19 | * calculate the coordinates of the envelope / bounding box: (min_lon, min_lat, max_lon, max_lat) 20 | */ 21 | extent() { 22 | return [ 23 | Math.min(...this.coords.map((c) => c[0])), 24 | Math.min(...this.coords.map((c) => c[1])), 25 | Math.max(...this.coords.map((c) => c[0])), 26 | Math.max(...this.coords.map((c) => c[1])), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/staticmaps.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import sharp from 'sharp'; 3 | import find from 'lodash.find'; 4 | import uniqBy from 'lodash.uniqby'; 5 | import url from 'url'; 6 | import chunk from 'lodash.chunk'; 7 | import { mapSeries } from 'modern-async'; 8 | 9 | import Image from './image'; 10 | import IconMarker from './marker'; 11 | import Polyline from './polyline'; 12 | import MultiPolygon from './multipolygon'; 13 | import Circle from './circle'; 14 | import Text from './text'; 15 | import Bound from './bound'; 16 | import TileServerConfig from './tileserverconfig'; 17 | 18 | import asyncQueue from './helper/asyncQueue'; 19 | import geoutils from './helper/geo'; 20 | 21 | const RENDER_CHUNK_SIZE = 1000; 22 | 23 | class StaticMaps { 24 | constructor(options = {}) { 25 | this.options = options; 26 | 27 | this.tileLayers = []; 28 | 29 | if (typeof this.options.tileLayers === 'undefined') { 30 | // Pulling from old options for backwards compatibility 31 | const baseLayerOptions = {}; 32 | if (this.options.tileUrl) { 33 | baseLayerOptions.tileUrl = this.options.tileUrl; 34 | } 35 | if (this.options.tileSubdomains) { 36 | baseLayerOptions.tileSubdomains = this.options.tileSubdomains; 37 | } 38 | this.tileLayers.push(new TileServerConfig(baseLayerOptions)); 39 | } else { 40 | this.options.tileLayers.forEach((layerConfig) => { 41 | this.tileLayers.push(new TileServerConfig(layerConfig)); 42 | }); 43 | } 44 | 45 | this.width = this.options.width; 46 | this.height = this.options.height; 47 | this.paddingX = this.options.paddingX || 0; 48 | this.paddingY = this.options.paddingY || 0; 49 | this.padding = [this.paddingX, this.paddingY]; 50 | this.tileSize = this.options.tileSize || 256; 51 | this.tileRequestTimeout = this.options.tileRequestTimeout; 52 | this.tileRequestHeader = this.options.tileRequestHeader; 53 | this.tileRequestLimit = Number.isFinite(this.options.tileRequestLimit) 54 | ? Number(this.options.tileRequestLimit) : 2; 55 | this.reverseY = this.options.reverseY || false; 56 | const zoomRange = this.options.zoomRange || {}; 57 | this.zoomRange = { 58 | min: zoomRange.min || 1, 59 | max: this.options.maxZoom || zoomRange.max || 17, // maxZoom 60 | }; 61 | // this.maxZoom = this.options.maxZoom; DEPRECATED: use zoomRange.max instead 62 | 63 | // # features 64 | this.markers = []; 65 | this.lines = []; 66 | this.multipolygons = []; 67 | this.circles = []; 68 | this.text = []; 69 | this.bounds = []; 70 | 71 | // # fields that get set when map is rendered 72 | this.center = []; 73 | this.centerX = 0; 74 | this.centerY = 0; 75 | this.zoom = 0; 76 | } 77 | 78 | addLine(options) { 79 | this.lines.push(new Polyline(options)); 80 | } 81 | 82 | addMarker(options) { 83 | this.markers.push(new IconMarker(options)); 84 | } 85 | 86 | addPolygon(options) { 87 | this.lines.push(new Polyline(options)); 88 | } 89 | 90 | addMultiPolygon(options) { 91 | this.multipolygons.push(new MultiPolygon(options)); 92 | } 93 | 94 | addCircle(options) { 95 | this.circles.push(new Circle(options)); 96 | } 97 | 98 | addBound(options) { 99 | this.bounds.push(new Bound(options)); 100 | } 101 | 102 | addText(options) { 103 | this.text.push(new Text(options)); 104 | } 105 | 106 | /** 107 | * Render static map with all map features that were added to map before 108 | */ 109 | async render(center, zoom) { 110 | if (!this.lines && !this.markers && !this.multipolygons && !(center && zoom)) { 111 | throw new Error('Cannot render empty map: Add center || lines || markers || polygons.'); 112 | } 113 | 114 | this.center = center; 115 | this.zoom = zoom || this.calculateZoom(); 116 | 117 | const maxZoom = this.zoomRange.max; 118 | if (maxZoom && this.zoom > maxZoom) this.zoom = maxZoom; 119 | 120 | if (center && center.length === 2) { 121 | this.centerX = geoutils.lonToX(center[0], this.zoom); 122 | this.centerY = geoutils.latToY(center[1], this.zoom); 123 | } else { 124 | // # get extent of all lines 125 | const extent = this.determineExtent(this.zoom); 126 | 127 | // # calculate center point of map 128 | const centerLon = (extent[0] + extent[2]) / 2; 129 | const centerLat = (extent[1] + extent[3]) / 2; 130 | 131 | this.centerX = geoutils.lonToX(centerLon, this.zoom); 132 | this.centerY = geoutils.latToY(centerLat, this.zoom); 133 | } 134 | 135 | this.image = new Image(this.options); 136 | 137 | // Await this.drawLayer for each tile layer 138 | await mapSeries(this.tileLayers, async (layer) => { 139 | await this.drawLayer(layer); 140 | }); 141 | 142 | await this.loadMarker(); 143 | return this.drawFeatures(); 144 | } 145 | 146 | /** 147 | * calculate common extent of all current map features 148 | */ 149 | determineExtent(zoom) { 150 | const extents = []; 151 | 152 | // Add bbox to extent 153 | if (this.center && this.center.length >= 4) extents.push(this.center); 154 | 155 | // add bounds to extent 156 | if (this.bounds.length) { 157 | this.bounds.forEach((bound) => extents.push(bound.extent())); 158 | } 159 | 160 | // Add polylines and polygons to extent 161 | if (this.lines.length) { 162 | this.lines.forEach((line) => { 163 | extents.push(line.extent()); 164 | }); 165 | } 166 | if (this.multipolygons.length) { 167 | this.multipolygons.forEach((multipolygon) => { 168 | extents.push(multipolygon.extent()); 169 | }); 170 | } 171 | 172 | // Add circles to extent 173 | if (this.circles.length) { 174 | this.circles.forEach((circle) => { 175 | extents.push(circle.extent()); 176 | }); 177 | } 178 | 179 | // Add marker to extent 180 | for (let i = 0; i < this.markers.length; i++) { 181 | const marker = this.markers[i]; 182 | const e = [marker.coord[0], marker.coord[1]]; 183 | 184 | if (!zoom) { 185 | extents.push([ 186 | marker.coord[0], 187 | marker.coord[1], 188 | marker.coord[0], 189 | marker.coord[1], 190 | ]); 191 | continue; 192 | } 193 | 194 | // # consider dimension of marker 195 | const ePx = marker.extentPx(); 196 | const x = geoutils.lonToX(e[0], zoom); 197 | const y = geoutils.latToY(e[1], zoom); 198 | 199 | extents.push([ 200 | geoutils.xToLon(x - parseFloat(ePx[0]) / this.tileSize, zoom), 201 | geoutils.yToLat(y + parseFloat(ePx[1]) / this.tileSize, zoom), 202 | geoutils.xToLon(x + parseFloat(ePx[2]) / this.tileSize, zoom), 203 | geoutils.yToLat(y - parseFloat(ePx[3]) / this.tileSize, zoom), 204 | ]); 205 | } 206 | 207 | return [ 208 | Math.min(...extents.map((e) => e[0])), 209 | Math.min(...extents.map((e) => e[1])), 210 | Math.max(...extents.map((e) => e[2])), 211 | Math.max(...extents.map((e) => e[3])), 212 | ]; 213 | } 214 | 215 | /** 216 | * calculate the best zoom level for given extent 217 | */ 218 | calculateZoom() { 219 | for (let z = this.zoomRange.max; z >= this.zoomRange.min; z--) { 220 | const extent = this.determineExtent(z); 221 | const width = (geoutils.lonToX(extent[2], z) 222 | - geoutils.lonToX(extent[0], z)) * this.tileSize; 223 | if (width > (this.width - (this.padding[0] * 2))) continue; 224 | 225 | const height = (geoutils.latToY(extent[1], z) 226 | - geoutils.latToY(extent[3], z)) * this.tileSize; 227 | if (height > (this.height - (this.padding[1] * 2))) continue; 228 | 229 | return z; 230 | } 231 | return this.zoomRange.min; 232 | } 233 | 234 | /** 235 | * transform tile number to pixel on image canvas 236 | */ 237 | xToPx(x) { 238 | const px = ((x - this.centerX) * this.tileSize) + (this.width / 2); 239 | return Number(Math.round(px)); 240 | } 241 | 242 | /** 243 | * transform tile number to pixel on image canvas 244 | */ 245 | yToPx(y) { 246 | const px = ((y - this.centerY) * this.tileSize) + (this.height / 2); 247 | return Number(Math.round(px)); 248 | } 249 | 250 | async drawLayer(config) { 251 | if (!config || !config.tileUrl) { 252 | // Early return if we shouldn't draw a base layer 253 | console.log(1); 254 | return this.image.draw([]); 255 | } 256 | const xMin = Math.floor(this.centerX - (0.5 * this.width / this.tileSize)); 257 | const yMin = Math.floor(this.centerY - (0.5 * this.height / this.tileSize)); 258 | const xMax = Math.ceil(this.centerX + (0.5 * this.width / this.tileSize)); 259 | const yMax = Math.ceil(this.centerY + (0.5 * this.height / this.tileSize)); 260 | 261 | const result = []; 262 | 263 | for (let x = xMin; x < xMax; x++) { 264 | for (let y = yMin; y < yMax; y++) { 265 | // # x and y may have crossed the date line 266 | const maxTile = (2 ** this.zoom); 267 | const tileX = (x + maxTile) % maxTile; 268 | let tileY = (y + maxTile) % maxTile; 269 | if (this.reverseY) tileY = ((1 << this.zoom) - tileY) - 1; 270 | 271 | let tileUrl; 272 | if (config.tileUrl.includes('{quadkey}')) { 273 | const quadKey = geoutils.tileXYToQuadKey(tileX, tileY, this.zoom); 274 | tileUrl = config.tileUrl.replace('{quadkey}', quadKey); 275 | } else { 276 | tileUrl = config.tileUrl.replace('{z}', this.zoom).replace('{x}', tileX).replace('{y}', tileY); 277 | } 278 | 279 | if (config.tileSubdomains.length > 0) { 280 | // replace subdomain with random domain from tileSubdomains array 281 | tileUrl = tileUrl.replace('{s}', config.tileSubdomains[Math.floor(Math.random() * config.tileSubdomains.length)]); 282 | } 283 | 284 | result.push({ 285 | url: tileUrl, 286 | box: [ 287 | this.xToPx(x), 288 | this.yToPx(y), 289 | this.xToPx(x + 1), 290 | this.yToPx(y + 1), 291 | ], 292 | }); 293 | } 294 | } 295 | 296 | const tiles = await this.getTiles(result); 297 | return this.image.draw(tiles.filter((v) => v.success).map((v) => v.tile)); 298 | } 299 | 300 | async drawSVG(features, svgFunction) { 301 | if (!features.length) return; 302 | 303 | // Chunk for performance 304 | const chunks = chunk(features, RENDER_CHUNK_SIZE); 305 | 306 | const baseImage = sharp(this.image.image); 307 | const imageMetadata = await baseImage.metadata(); 308 | 309 | const processedChunks = chunks.map((c) => { 310 | const svg = ` 311 | 316 | ${c.map((f) => svgFunction(f)).join('\n')} 317 | 318 | `; 319 | return { input: Buffer.from(svg), top: 0, left: 0 }; 320 | }); 321 | 322 | this.image.image = await baseImage 323 | .composite(processedChunks) 324 | .toBuffer(); 325 | } 326 | 327 | /** 328 | * Render a circle to SVG 329 | */ 330 | circleToSVG(circle) { 331 | const latCenter = circle.coord[1]; 332 | const radiusInPixel = geoutils.meterToPixel(circle.radius, this.zoom, latCenter); 333 | const x = this.xToPx(geoutils.lonToX(circle.coord[0], this.zoom)); 334 | const y = this.yToPx(geoutils.latToY(circle.coord[1], this.zoom)); 335 | return ` 336 | 345 | `; 346 | } 347 | 348 | /** 349 | * Render text to SVG 350 | */ 351 | textToSVG(text) { 352 | const mapcoords = [ 353 | this.xToPx(geoutils.lonToX(text.coord[0], this.zoom)) - text.offset[0], 354 | this.yToPx(geoutils.latToY(text.coord[1], this.zoom)) - text.offset[1], 355 | ]; 356 | 357 | return ` 358 | 368 | ${text.text} 369 | 370 | `; 371 | } 372 | 373 | /** 374 | * Render MultiPolygon to SVG 375 | */ 376 | multiPolygonToSVG(multipolygon) { 377 | const shapeArrays = multipolygon.coords.map((shape) => shape.map((coord) => [ 378 | this.xToPx(geoutils.lonToX(coord[0], this.zoom)), 379 | this.yToPx(geoutils.latToY(coord[1], this.zoom)), 380 | ])); 381 | 382 | const pathArrays = shapeArrays.map((points) => { 383 | const startPoint = points.shift(); 384 | 385 | const pathParts = [ 386 | `M ${startPoint[0]} ${startPoint[1]}`, 387 | ...points.map((p) => `L ${p[0]} ${p[1]}`), 388 | 'Z', 389 | ]; 390 | 391 | return pathParts.join(' '); 392 | }); 393 | 394 | return ``; 400 | } 401 | 402 | /** 403 | * Render Polyline to SVG 404 | */ 405 | lineToSVG(line) { 406 | const points = line.coords.map((coord) => [ 407 | this.xToPx(geoutils.lonToX(coord[0], this.zoom)), 408 | this.yToPx(geoutils.latToY(coord[1], this.zoom)), 409 | ]); 410 | return `<${(line.type === 'polyline') ? 'polyline' : 'polygon'} 411 | style="fill-rule: inherit;" 412 | points="${points.join(' ')}" 413 | stroke="${line.color}" 414 | fill="${line.fill ? line.fill : 'none'}" 415 | stroke-width="${line.width}"/>`; 416 | } 417 | 418 | /** 419 | * Draw markers to the basemap 420 | */ 421 | drawMarkers() { 422 | const queue = []; 423 | this.markers.forEach((marker) => { 424 | queue.push(async () => { 425 | const top = Math.round(marker.position[1]); 426 | const left = Math.round(marker.position[0]); 427 | 428 | if ( 429 | top < 0 430 | || left < 0 431 | || top > this.height 432 | || left > this.width 433 | ) return; 434 | 435 | const markerInstance = await sharp(marker.imgData); 436 | 437 | if (marker.width === null || marker.height === null) { 438 | const metadata = await markerInstance.metadata(); 439 | 440 | if (Number.isFinite(metadata.width) && Number.isFinite(metadata.height)) { 441 | marker.setSize(metadata.width, metadata.height); 442 | } else { 443 | throw new Error(`Cannot detectimage size of marker ${marker.img}. Please define manually!`); 444 | } 445 | } 446 | 447 | // Check if resize marker image is needed 448 | if ( 449 | marker.drawWidth !== marker.width 450 | || marker.drawHeight !== marker.height 451 | ) { 452 | const resizeData = { 453 | fit: marker.resizeMode, 454 | }; 455 | 456 | if (marker.drawWidth !== marker.width) { 457 | resizeData.width = marker.drawWidth; 458 | } 459 | 460 | if (marker.drawHeight !== marker.height) { 461 | resizeData.height = marker.drawHeight; 462 | } 463 | 464 | await markerInstance 465 | .resize(resizeData); 466 | } 467 | 468 | this.image.image = await sharp(this.image.image) 469 | .composite([{ 470 | input: await markerInstance.toBuffer(), 471 | top, 472 | left, 473 | }]) 474 | .toBuffer(); 475 | }); 476 | }); 477 | return asyncQueue(queue); 478 | } 479 | 480 | /** 481 | * Draw all features to the basemap 482 | */ 483 | async drawFeatures() { 484 | await this.drawSVG(this.lines, (c) => this.lineToSVG(c)); 485 | await this.drawSVG(this.multipolygons, (c) => this.multiPolygonToSVG(c)); 486 | await this.drawMarkers(); 487 | await this.drawSVG(this.text, (c) => this.textToSVG(c)); 488 | await this.drawSVG(this.circles, (c) => this.circleToSVG(c)); 489 | } 490 | 491 | /** 492 | * Preloading the icon image 493 | */ 494 | loadMarker() { 495 | return new Promise((resolve, reject) => { 496 | if (!this.markers.length) resolve(true); 497 | const icons = uniqBy(this.markers.map((m) => ({ file: m.img })), 'file'); 498 | 499 | let count = 1; 500 | icons.forEach(async (ico) => { 501 | const icon = ico; 502 | const isUrl = !!url.parse(icon.file).hostname; 503 | try { 504 | // Load marker from remote url 505 | if (isUrl) { 506 | const img = await got.get({ 507 | https: { 508 | rejectUnauthorized: false, 509 | }, 510 | url: icon.file, 511 | responseType: 'buffer', 512 | }); 513 | icon.data = await sharp(img.body).toBuffer(); 514 | } else { 515 | // Load marker from local fs 516 | icon.data = await sharp(icon.file).toBuffer(); 517 | } 518 | } catch (err) { 519 | reject(err); 520 | } 521 | 522 | if (count++ === icons.length) { 523 | // Pre loaded all icons 524 | this.markers.forEach((mark) => { 525 | const marker = mark; 526 | marker.position = [ 527 | this.xToPx(geoutils.lonToX(marker.coord[0], this.zoom)) - marker.offset[0], 528 | this.yToPx(geoutils.latToY(marker.coord[1], this.zoom)) - marker.offset[1], 529 | ]; 530 | const imgData = find(icons, { file: marker.img }); 531 | marker.set(imgData.data); 532 | }); 533 | 534 | resolve(true); 535 | } 536 | }); 537 | }); 538 | } 539 | 540 | /** 541 | * Fetching tile from endpoint 542 | */ 543 | async getTile(data) { 544 | const options = { 545 | url: data.url, 546 | responseType: 'buffer', 547 | // resolveWithFullResponse: true, 548 | headers: this.tileRequestHeader || {}, 549 | timeout: this.tileRequestTimeout, 550 | }; 551 | 552 | try { 553 | const res = await got.get(options); 554 | const { body, headers } = res; 555 | 556 | const contentType = headers['content-type']; 557 | if (!contentType.startsWith('image/')) throw new Error('Tiles server response with wrong data'); 558 | // console.log(headers); 559 | 560 | return { 561 | success: true, 562 | tile: { 563 | url: data.url, 564 | box: data.box, 565 | body, 566 | }, 567 | }; 568 | } catch (error) { 569 | return { 570 | success: false, 571 | error, 572 | }; 573 | } 574 | } 575 | 576 | /** 577 | * Fetching tiles and limit concurrent connections 578 | */ 579 | async getTiles(baseLayers) { 580 | const limit = this.tileRequestLimit; 581 | 582 | // Limit concurrent connections to tiles server 583 | // https://operations.osmfoundation.org/policies/tiles/#technical-usage-requirements 584 | if (Number(limit)) { 585 | const aQueue = []; 586 | const tiles = []; 587 | for (let i = 0, j = baseLayers.length; i < j; i += limit) { 588 | const chunks = baseLayers.slice(i, i + limit); 589 | const sQueue = []; 590 | aQueue.push(async () => { 591 | chunks.forEach((r) => { 592 | sQueue.push((async () => { 593 | const tile = await this.getTile(r); 594 | tiles.push(tile); 595 | })()); 596 | }); 597 | await Promise.all(sQueue); 598 | }); 599 | } 600 | await asyncQueue(aQueue); 601 | return tiles; 602 | } 603 | 604 | // Do not limit concurrent connections at all 605 | const tilePromises = []; 606 | baseLayers.forEach((r) => { tilePromises.push(this.getTile(r)); }); 607 | return Promise.all(tilePromises); 608 | } 609 | } 610 | 611 | export default StaticMaps; 612 | module.exports = StaticMaps; 613 | -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | export default class Text { 2 | constructor(options = {}) { 3 | this.options = options; 4 | this.coord = this.options.coord; 5 | this.text = this.options.text; 6 | this.color = this.options.color || '#000000BB'; 7 | this.width = `${this.options.width}px` || '1px'; 8 | this.fill = this.options.fill || '#000000'; 9 | this.size = this.options.size || 12; 10 | this.font = this.options.font || 'Arial'; 11 | this.anchor = this.options.anchor || 'start'; 12 | this.offsetX = Number.isFinite(this.options.offsetX) 13 | ? Number(this.options.offsetX) : 0; 14 | this.offsetY = Number.isFinite(this.options.offsetY) 15 | ? Number(this.options.offsetY) : 0; 16 | this.offset = [this.offsetX, this.offsetY]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tileserverconfig.js: -------------------------------------------------------------------------------- 1 | class TileServerConfig { 2 | constructor(options) { 3 | this.options = options; 4 | this.tileUrl = 'tileUrl' in this.options ? this.options.tileUrl : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; 5 | this.tileSubdomains = this.options.tileSubdomains || this.options.subdomains || []; 6 | } 7 | } 8 | 9 | export default TileServerConfig; 10 | module.exports = TileServerConfig; 11 | -------------------------------------------------------------------------------- /test/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StephanGeorg/staticmaps/1ba242f0dd364474a33e55e5c34a64bc8a93e1f8/test/marker.png -------------------------------------------------------------------------------- /test/out/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | -------------------------------------------------------------------------------- /test/static/geojson.js: -------------------------------------------------------------------------------- 1 | export default { 2 | way: { 3 | type: 'Feature', 4 | properties: {}, 5 | geometry: { 6 | type: 'MultiPolygon', 7 | coordinates: [ 8 | [ 9 | [ 10 | [13.3682291339579, 52.4933357148621], 11 | [13.3685806447286, 52.4939555058339], 12 | [13.3684148157271, 52.4944780990048], 13 | [13.368452185643, 52.4949242809123], 14 | [13.3684812012266, 52.4952230638387], 15 | [13.3685269254746, 52.49573891492], 16 | [13.3685389628994, 52.4958760804103], 17 | [13.3685599834771, 52.49607706974], 18 | [13.3685912448489, 52.4964110654868], 19 | [13.3685916041751, 52.4964588102094], 20 | [13.368596185583, 52.4965107660343], 21 | [13.3686038212629, 52.4965258058669], 22 | [13.3686361606132, 52.4968769708226], 23 | [13.3686779322739, 52.497384708497], 24 | [13.3696009512283, 52.4981061081144], 25 | [13.3697288713248, 52.4987764724956], 26 | [13.3695331284243, 52.4988697692571], 27 | [13.370373053215, 52.4995143099397], 28 | [13.370362003937, 52.4995304971625], 29 | [13.3705521772827, 52.4996846036005], 30 | [13.3705545129024, 52.4996818145959], 31 | [13.3705870319157, 52.4996835645596], 32 | [13.370825085466, 52.4998602011579], 33 | [13.3708884166935, 52.4999543705961], 34 | [13.3713442218687, 52.5016339651258], 35 | [13.3714530976811, 52.5017579881203], 36 | [13.3717360669956, 52.5020391165492], 37 | [13.3721632159132, 52.5024871903173], 38 | [13.3730430259025, 52.5033822851989], 39 | [13.3736093238576, 52.5041640087235], 40 | [13.3742441632689, 52.5034854695635], 41 | [13.3746784088772, 52.5032355737856], 42 | [13.374752160562, 52.5032344801466], 43 | [13.3749797038235, 52.5033758874322], 44 | [13.3776389865591, 52.507909100526], 45 | [13.3776503053316, 52.5079657996375], 46 | [13.3789222299424, 52.5069994989946], 47 | [13.3817166192967, 52.5071340048552], 48 | [13.3857617330211, 52.5073286002071], 49 | [13.3868832796534, 52.5073950872106], 50 | [13.3898682016794, 52.5075710913829], 51 | [13.3904839968067, 52.5075984843268], 52 | [13.3922256504795, 52.5077032990862], 53 | [13.3942838704585, 52.5078332099903], 54 | [13.3981887571671, 52.5080788695856], 55 | [13.398771853618, 52.5081133700776], 56 | [13.3987944013316, 52.508091663749], 57 | [13.3992300842444, 52.5080775026876], 58 | [13.3997670971213, 52.5087999843794], 59 | [13.3999386753405, 52.5090281973328], 60 | [13.4000253627654, 52.5091509966252], 61 | [13.4001885866526, 52.5093754899033], 62 | [13.4002282921881, 52.5093816134253], 63 | [13.4009174796741, 52.5091377653734], 64 | [13.4012637802161, 52.5090380934595], 65 | [13.4021012795555, 52.5087284693912], 66 | [13.4027619006155, 52.5085333881588], 67 | [13.4036334461041, 52.5081777782137], 68 | [13.4040369693297, 52.5079940671518], 69 | [13.4043011638548, 52.5078616962946], 70 | [13.4044429180066, 52.507771808546], 71 | [13.4045782042884, 52.5078855897833], 72 | [13.4046342591621, 52.5079227695567], 73 | [13.405283651281, 52.5082102009494], 74 | [13.4056214178279, 52.5079614802285], 75 | [13.4057964994767, 52.5078344675435], 76 | [13.4059382536286, 52.5077298718023], 77 | [13.4064043894295, 52.5073853000675], 78 | [13.4073588494189, 52.5066478134334], 79 | [13.4074001719219, 52.5066723090363], 80 | [13.4078500482162, 52.5063266901267], 81 | [13.4080300705992, 52.5061832145589], 82 | [13.4099693536345, 52.5069277623673], 83 | [13.4108938997249, 52.5057255003088], 84 | [13.4115251458751, 52.504891093175], 85 | [13.4140723188632, 52.5040370944267], 86 | [13.4146096910662, 52.5046798095894], 87 | [13.4145925332443, 52.5047920677907], 88 | [13.4149072130883, 52.5049184877891], 89 | [13.4166273071943, 52.5044415128569], 90 | [13.4166966571342, 52.5044216091704], 91 | [13.4168472147759, 52.5043797785913], 92 | [13.4176016199515, 52.5041684925481], 93 | [13.418213552323, 52.5050176769278], 94 | [13.4184001324075, 52.5049932897623], 95 | [13.4185186201935, 52.504986673511], 96 | [13.4185318254282, 52.5049690119428], 97 | [13.4189443318066, 52.5050602724153], 98 | [13.4189244790389, 52.5050800117736], 99 | [13.4189980510606, 52.5051181781531], 100 | [13.4191100709766, 52.5052036969691], 101 | [13.4194305000384, 52.5056491138924], 102 | [13.4206173541918, 52.5053213670885], 103 | [13.4211304718821, 52.5051753729807], 104 | [13.4215419002822, 52.5050784807433], 105 | [13.4218108558783, 52.5050420094], 106 | [13.422078733496, 52.5050273005588], 107 | [13.4223010665288, 52.5050077798959], 108 | [13.4226641655667, 52.5049871656289], 109 | [13.4230379545564, 52.504980002579], 110 | [13.4233525445689, 52.5049867828705], 111 | [13.4237378319942, 52.5050089828501], 112 | [13.4239799279633, 52.5050292690286], 113 | [13.4243498541973, 52.5050747625268], 114 | [13.4244748098533, 52.5050932989267], 115 | [13.4247138515504, 52.505132996323], 116 | [13.4249568458348, 52.5051818798445], 117 | [13.4252194233923, 52.5052477139419], 118 | [13.4255040995059, 52.5053238823385], 119 | [13.4258214742957, 52.5054247109391], 120 | [13.4261207031169, 52.5055508010436], 121 | [13.4267082911442, 52.5057986058673], 122 | [13.4269722161747, 52.5057311869086], 123 | [13.4271996696046, 52.5056673766554], 124 | [13.4274989882573, 52.5060934874128], 125 | [13.4276138827821, 52.5062577956671], 126 | [13.4276889819399, 52.5063666051214], 127 | [13.427853732963, 52.5066012826859], 128 | [13.4279957566094, 52.506804410088], 129 | [13.4281137054062, 52.5069734726098], 130 | [13.4283664913272, 52.5073345053069], 131 | [13.4288143913278, 52.507977609665], 132 | [13.429028280197, 52.5080362770233], 133 | [13.4294017098606, 52.5085625847885], 134 | [13.4295928713531, 52.5084810638133], 135 | [13.4305703282137, 52.5080101965764], 136 | [13.4322017586012, 52.5072879752863], 137 | [13.433985902587, 52.5063731664869], 138 | [13.4343958936827, 52.5060961666538], 139 | [13.4354966892318, 52.5056383968183], 140 | [13.4358479305079, 52.5054102756222], 141 | [13.4359798930232, 52.505369977004], 142 | [13.4360036085467, 52.5053823892011], 143 | [13.4361070944674, 52.5053271084199], 144 | [13.4360790670305, 52.5053102124996], 145 | [13.4361593764169, 52.5052710620574], 146 | [13.4361205691966, 52.5052418085618], 147 | [13.4368857541557, 52.5047220773238], 148 | [13.4372358276219, 52.504535672482], 149 | [13.4380538335196, 52.5042168850157], 150 | [13.4384861926658, 52.5040257754691], 151 | [13.4389878119205, 52.5038294703773], 152 | [13.4414478483261, 52.5029236668653], 153 | [13.44482290868, 52.5012990794192], 154 | [13.4452293065146, 52.5010995904442], 155 | [13.4453706115088, 52.5010389998636], 156 | [13.4454446326882, 52.5010049859812], 157 | [13.4456756793792, 52.500883695106], 158 | [13.445973650559, 52.5007421704336], 159 | [13.4465146160231, 52.5005139691425], 160 | [13.4467998311258, 52.5004083716429], 161 | [13.4479068250504, 52.5000628128527], 162 | [13.4479799479145, 52.5000243139682], 163 | [13.4485088759538, 52.4998567012446], 164 | [13.4486051753523, 52.4998262957359], 165 | [13.4490124715021, 52.4997046734907], 166 | [13.4496831536932, 52.4994836854477], 167 | [13.4496399447281, 52.4994489047488], 168 | [13.4497137862444, 52.4994105147002], 169 | [13.4501111110946, 52.4992209154926], 170 | [13.4501977985195, 52.499175798745], 171 | [13.4503934515884, 52.4990557059405], 172 | [13.4504627116968, 52.4990111905826], 173 | [13.4506721089895, 52.498928667521], 174 | [13.4509071082678, 52.4989139019469], 175 | [13.4514407973781, 52.4989365971791], 176 | [13.4515688073061, 52.4988624958356], 177 | [13.4521464240338, 52.4984785887203], 178 | [13.452230955502, 52.4985063154576], 179 | [13.4526908929275, 52.4982566101281], 180 | [13.4527234119408, 52.4982390005441], 181 | [13.452782610918, 52.4980971939164], 182 | [13.4527550326388, 52.4980776154876], 183 | [13.45272395093, 52.4980556854495], 184 | [13.4526917014113, 52.4979008076618], 185 | [13.4527550326388, 52.4978474862633], 186 | [13.4529295752985, 52.4977067721494], 187 | [13.4520130242141, 52.4975231813906], 188 | [13.4513876171133, 52.4973922009198], 189 | [13.4510076297481, 52.4972687125028], 190 | [13.4506551308306, 52.4970891122551], 191 | [13.4500550562209, 52.4967182058894], 192 | [13.4474502113915, 52.4948733076751], 193 | [13.4469089764328, 52.4948401094084], 194 | [13.4455485677665, 52.4948559701979], 195 | [13.4451615735421, 52.4945449882946], 196 | [13.4446427964656, 52.4941677707478], 197 | [13.4440676950207, 52.4937294023739], 198 | [13.4423283769676, 52.4924037148177], 199 | [13.4406859871336, 52.4910888720121], 200 | [13.4404055331019, 52.4909350636296], 201 | [13.4401045076502, 52.4907393017559], 202 | [13.4398843305741, 52.4906049098227], 203 | [13.4398106687208, 52.4905073837956], 204 | [13.439765483462, 52.4903858998665], 205 | [13.4397151778061, 52.4902455994587], 206 | [13.4396814909829, 52.4901288735543], 207 | [13.4396797841839, 52.490032166947], 208 | [13.4396363057241, 52.4899001795884], 209 | [13.4396418752789, 52.4898351976849], 210 | [13.4396337006098, 52.4896945126289], 211 | [13.4396160037987, 52.4896917776894], 212 | [13.4396277717289, 52.4896585755109], 213 | [13.4394355322581, 52.4896335781376], 214 | [13.4392625167344, 52.4896095106312], 215 | [13.4392535335815, 52.489644080318], 216 | [13.4392337706453, 52.4896407983869], 217 | [13.4392280214275, 52.4896601070782], 218 | [13.4385659630631, 52.4901511904334], 219 | [13.438363303135, 52.4903264978771], 220 | [13.4382886531349, 52.4903852981893], 221 | [13.4372844264788, 52.4906861904631], 222 | [13.4371352163101, 52.4907395752434], 223 | [13.4365474486197, 52.4909388924365], 224 | [13.435298161554, 52.4913200758481], 225 | [13.4329810471102, 52.4920568898054], 226 | [13.430664561487, 52.4927925975193], 227 | [13.4298524844702, 52.4930558981443], 228 | [13.4290746332657, 52.4933098991644], 229 | [13.4282946261045, 52.4935637893294], 230 | [13.4277766575116, 52.4937324652249], 231 | [13.4275707636485, 52.4937991915679], 232 | [13.4270498306153, 52.4939685775755], 233 | [13.4266613092549, 52.4940950833933], 234 | [13.4262867117814, 52.4942171040081], 235 | [13.4259279246569, 52.4943340925269], 236 | [13.4252665849447, 52.4945484886308], 237 | [13.4249347472788, 52.4946563972947], 238 | [13.4241330907192, 52.4949170068381], 239 | [13.4233159831368, 52.4951828652655], 240 | [13.4230668803085, 52.4952638639855], 241 | [13.4227461817521, 52.4953664111044], 242 | [13.4224898924015, 52.4954451671294], 243 | [13.4224259323533, 52.4954629965988], 244 | [13.4222248893927, 52.4955197664563], 245 | [13.4219508133995, 52.4955904825636], 246 | [13.4216680237481, 52.4956564950993], 247 | [13.4213920612928, 52.4957133740896], 248 | [13.4209690446255, 52.495788902712], 249 | [13.4207201214603, 52.4958347886112], 250 | [13.4204842238667, 52.4958649781158], 251 | [13.4203998720615, 52.4958637749113], 252 | [13.4204623948053, 52.4957902699922], 253 | [13.4206390035901, 52.4956194143334], 254 | [13.4207907290416, 52.4954739896116], 255 | [13.4215921161066, 52.4941974144652], 256 | [13.4217059326531, 52.4940225053054], 257 | [13.4222972037731, 52.4930834095229], 258 | [13.4223996117155, 52.4929191067202], 259 | [13.4227769041348, 52.4923142877186], 260 | [13.4229476738703, 52.4920411920939], 261 | [13.4234828901166, 52.4911805988746], 262 | [13.4235851183959, 52.4910170000255], 263 | [13.4239800177948, 52.4903875955022], 264 | [13.4241966914414, 52.4900392777342], 265 | [13.4242970332586, 52.4898778078833], 266 | [13.4249188470983, 52.4888764846568], 267 | [13.4253265924057, 52.4882338666565], 268 | [13.4253499486031, 52.4881820104534], 269 | [13.4253550690002, 52.4879987081316], 270 | [13.4249380710453, 52.4877801775077], 271 | [13.4241585130418, 52.4868284762881], 272 | [13.4238568587694, 52.4865328637859], 273 | [13.4237046841602, 52.4863836892172], 274 | [13.4231691085879, 52.4865766806423], 275 | [13.4212559665273, 52.4871636910222], 276 | [13.4176928887843, 52.4876358755894], 277 | [13.4085594477961, 52.4887698746082], 278 | [13.4078874181321, 52.488860184112], 279 | [13.4077227569405, 52.4887470647284], 280 | [13.4077568030897, 52.4886622796999], 281 | [13.4078375616338, 52.4884842853083], 282 | [13.407912391297, 52.4884776665725], 283 | [13.4083374740894, 52.4874687626549], 284 | [13.4084231733675, 52.4871133105114], 285 | [13.4082859107921, 52.4868939002521], 286 | [13.4073190540518, 52.4860657000817], 287 | [13.4067496119932, 52.4854768698154], 288 | [13.4064589171672, 52.4835903060913], 289 | [13.4063457294415, 52.4827922969059], 290 | [13.4048859671048, 52.4830993136016], 291 | [13.4034421049486, 52.4833998727745], 292 | [13.4023152582562, 52.4835846713339], 293 | [13.401225511985, 52.4837387789527], 294 | [13.400147174318, 52.4838406964744], 295 | [13.3993829775058, 52.4838929954437], 296 | [13.3987168767226, 52.4839327666894], 297 | [13.3979566324976, 52.4839486861189], 298 | [13.3946131030101, 52.4840216090673], 299 | [13.3942340139603, 52.4841256047508], 300 | [13.3942336546341, 52.4843357835984], 301 | [13.3942367089061, 52.484540107986], 302 | [13.3942421886293, 52.4849395088206], 303 | [13.3942454225644, 52.4850677911572], 304 | [13.3942501836354, 52.4853328885659], 305 | [13.3942540463911, 52.4854937733583], 306 | [13.394258627799, 52.4857241860518], 307 | [13.3942612329134, 52.4857752793013], 308 | [13.3910654762901, 52.4858053116012], 309 | [13.3862812287499, 52.4858228714611], 310 | [13.3862732337439, 52.4852298804146], 311 | [13.3862683828414, 52.4848587099031], 312 | [13.3848478768826, 52.4848715655296], 313 | [13.3847829286876, 52.4849106794337], 314 | [13.376854757316, 52.4849651653178], 315 | [13.3716065299316, 52.4849790055969], 316 | [13.3717189990052, 52.485173863135], 317 | [13.3740185064695, 52.4851670797877], 318 | [13.3740954022578, 52.4861754894685], 319 | [13.3741755319811, 52.4877317669472], 320 | [13.3734817630872, 52.4877504747633], 321 | [13.3735179651932, 52.4879702636563], 322 | [13.3746060046653, 52.4891098881042], 323 | [13.3748891536429, 52.4893188944522], 324 | [13.375412961285, 52.4893656075907], 325 | [13.3754647940769, 52.4904496776036], 326 | [13.3756924271699, 52.4906808848], 327 | [13.3760500464845, 52.4910506934524], 328 | [13.3760885842102, 52.4911467961532], 329 | [13.376465607135, 52.4915150693852], 330 | [13.3765622658595, 52.4916704072365], 331 | [13.3723923761422, 52.492530170442], 332 | [13.3720177786687, 52.492587272257], 333 | [13.3710648458153, 52.4927373009061], 334 | [13.3710815544796, 52.4929041750435], 335 | [13.3692526743926, 52.4932858883642], 336 | [13.3691729039954, 52.4931505744066], 337 | [13.3682291339579, 52.4933357148621], 338 | ], 339 | ], 340 | ], 341 | }, 342 | }, 343 | osm_id: 55765, 344 | admin_level: 10, 345 | }; 346 | -------------------------------------------------------------------------------- /test/static/multipolygonGeometry.js: -------------------------------------------------------------------------------- 1 | export default { 2 | geometry: { 3 | type: 'Polygon', 4 | coordinates: [ 5 | [ 6 | [-89.9619685, 41.7792032], 7 | [-89.959505, 41.7792084], 8 | [-89.9594928, 41.7827904], 9 | [-89.9631906, 41.7827815], 10 | [-89.9632678, 41.7821559], 11 | [-89.9634801, 41.7805341], 12 | [-89.9635341, 41.780109], 13 | [-89.9635792, 41.7796834], 14 | [-89.9636183, 41.7792165], 15 | [-89.9619685, 41.7792032], 16 | ], 17 | [ 18 | [-89.9631647, 41.7809413], 19 | [-89.9632927, 41.7809487], 20 | [-89.9631565, 41.781985], 21 | [-89.9622404, 41.7819137], 22 | [-89.9623616, 41.780997], 23 | [-89.963029, 41.7810114], 24 | [-89.9631647, 41.7809413], 25 | ], 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /test/static/route.js: -------------------------------------------------------------------------------- 1 | export default { 2 | code: 'Ok', 3 | routes: [{ 4 | geometry: { 5 | coordinates: [ 6 | [13.438854, 52.494917], 7 | [13.426408, 52.499321], 8 | [13.411286, 52.503481], 9 | [13.411046, 52.504033], 10 | [13.410159, 52.504214], 11 | [13.410085, 52.504713], 12 | [13.405126, 52.508227], 13 | [13.405304, 52.508327], 14 | [13.401721, 52.511327], 15 | [13.401397, 52.511232], 16 | [13.401094, 52.511636], 17 | [13.397173, 52.513344], 18 | [13.395061, 52.513335], 19 | [13.394641, 52.514157], 20 | [13.393179, 52.514105], 21 | [13.393033, 52.514997], 22 | [13.389354, 52.514774], 23 | [13.389037, 52.515649], 24 | [13.386058, 52.515451], 25 | [13.385894, 52.516325], 26 | [13.38652, 52.51636], 27 | ], 28 | type: 'LineString', 29 | }, 30 | legs: [{ 31 | summary: '', 32 | weight: 3490.8, 33 | duration: 3490.8, 34 | steps: [], 35 | distance: 4827.1, 36 | }], 37 | weight_name: 'duration', 38 | weight: 3490.8, 39 | duration: 3490.8, 40 | distance: 4827.1, 41 | }], 42 | waypoints: [{ 43 | hint: '0qodgOKDNILw0AIASgUAAAAAAAAAAAAAUgAAAEoFAAAAAAAAAAAAAFIAAAACphYABwAAAIYPzQBFAiEDhg_NAEUCIQMAAAMDPkaMgQ==', 44 | name: 'Glogauer Straße', 45 | location: [13.438854, 52.494917], 46 | }, { 47 | hint: 'gNt0jZVKepEAAAAAMgEAAAAAAAAAAAAAAAAAADIBAAAAAAAAAAAAAAAAAAC7m5EHBwAAABhDzAAIViEDJEbMAARVIQMAAAMDPkaMgQ==', 48 | name: '', 49 | location: [13.38652, 52.51636], 50 | }], 51 | }; 52 | -------------------------------------------------------------------------------- /test/static/routeLong.js: -------------------------------------------------------------------------------- 1 | export default { 2 | code: 'Ok', 3 | routes: [{ 4 | geometry: { 5 | coordinates: [ 6 | [ 7 | -93.5682337638862, 8 | 46.0, 9 | ], 10 | [ 11 | -93.1, 12 | 47.0, 13 | ], 14 | [ 15 | -93.5682337638862, 16 | 45.366349609687, 17 | ], 18 | [ 19 | -93.5682337638862, 20 | 45.366349609687, 21 | ], 22 | [ 23 | -93.5682337638862, 24 | 45.3566349609687, 25 | ], 26 | [ 27 | -93.5682337638862, 28 | 45.376349609687, 29 | ], 30 | [ 31 | -93.5682337638862, 32 | 45.26466349609687, 33 | ], 34 | [ 35 | -93.5682337638862, 36 | 45.26466349609687, 37 | ], 38 | 39 | [ 40 | -93.5682337638862, 41 | 45.26466349609687, 42 | ], 43 | [ 44 | -93.55257678785692, 45 | 45.19870994616784, 46 | ], 47 | [ 48 | -93.44737744898856, 49 | 45.09250905368951, 50 | ], 51 | [ 52 | -93.16555649050804, 53 | 45.065913484473775, 54 | ], 55 | [ 56 | -93.09093399913338, 57 | 45.03538030669562, 58 | ], 59 | [ 60 | -93.09108554394273, 61 | 44.95470415339335, 62 | ], 63 | [ 64 | -93.04085665391108, 65 | 44.94867882229848, 66 | ], 67 | [ 68 | -92.99072968782204, 69 | 44.850969971199376, 70 | ], 71 | [ 72 | -92.8456739802778, 73 | 44.76046561733332, 74 | ], 75 | [ 76 | -92.64610061430618, 77 | 44.70518466089359, 78 | ], 79 | [ 80 | -92.62866851882322, 81 | 44.65014237914667, 82 | ], 83 | [ 84 | -92.52050994904016, 85 | 44.59643810060303, 86 | ], 87 | [ 88 | -92.42375043229426, 89 | 44.59431907166224, 90 | ], 91 | [ 92 | -92.31866969735576, 93 | 44.56655129328795, 94 | ], 95 | [ 96 | -92.28503362284256, 97 | 44.49698011395009, 98 | ], 99 | [ 100 | -92.21315251671916, 101 | 44.46069225673048, 102 | ], 103 | [ 104 | -92.1354429704064, 105 | 44.43885706369247, 106 | ], 107 | [ 108 | -92.01377302416788, 109 | 44.433677969541804, 110 | ], 111 | [ 112 | -92.0379092517842, 113 | 44.37291057851025, 114 | ], 115 | [ 116 | -92.00384855990195, 117 | 44.33656974023691, 118 | ], 119 | [ 120 | -91.98194872514269, 121 | 44.2633904296932, 122 | ], 123 | [ 124 | -91.94718289690334, 125 | 44.22770974738392, 126 | ], 127 | [ 128 | -91.8150375928654, 129 | 44.16047411338978, 130 | ], 131 | [ 132 | -91.72415855217784, 133 | 44.06880142165885, 134 | ], 135 | [ 136 | -91.62061589331654, 137 | 44.02901631432496, 138 | ], 139 | [ 140 | -91.48124967710253, 141 | 44.006516654085374, 142 | ], 143 | [ 144 | -91.3732516207652, 145 | 43.93573563084373, 146 | ], 147 | [ 148 | -91.3019218762324, 149 | 43.8537484640664, 150 | ], 151 | [ 152 | -91.29703774125436, 153 | 43.825291986618026, 154 | ], 155 | [ 156 | -91.26839510173654, 157 | 43.81765795875696, 158 | ], 159 | [ 160 | -91.20028451093584, 161 | 43.74676147479913, 162 | ], 163 | [ 164 | -91.22938781985196, 165 | 43.56105668008659, 166 | ], 167 | [ 168 | -91.2036595680669, 169 | 43.427162566262695, 170 | ], 171 | [ 172 | -91.16568124861564, 173 | 43.38039624043124, 174 | ], 175 | [ 176 | -91.07558308177728, 177 | 43.33560435571087, 178 | ], 179 | [ 180 | -91.04942064740791, 181 | 43.29293610997805, 182 | ], 183 | [ 184 | -91.0584015213801, 185 | 43.24392502665822, 186 | ], 187 | [ 188 | -91.11901584089836, 189 | 43.190942555706194, 190 | ], 191 | [ 192 | -91.14172945738876, 193 | 43.14620879948432, 194 | ], 195 | [ 196 | -91.13576246433996, 197 | 43.06610374715465, 198 | ], 199 | [ 200 | -91.148752151142, 201 | 43.0414770869939, 202 | ], 203 | [ 204 | -91.11018793665907, 205 | 43.01677301995737, 206 | ], 207 | [ 208 | -91.04007013150589, 209 | 43.01333375744802, 210 | ], 211 | [ 212 | -90.9294452705336, 213 | 43.07900806427288, 214 | ], 215 | [ 216 | -90.73948871352778, 217 | 43.13820186075115, 218 | ], 219 | [ 220 | -90.64958324664332, 221 | 43.20153501820117, 222 | ], 223 | [ 224 | -90.55372268901256, 225 | 43.21976632815154, 226 | ], 227 | [ 228 | -90.47231772922954, 229 | 43.21966909807475, 230 | ], 231 | [ 232 | -90.42651272387285, 233 | 43.20234957155138, 234 | ], 235 | [ 236 | -90.29980315835245, 237 | 43.22314285211565, 238 | ], 239 | [ 240 | -90.22408490077612, 241 | 43.19267023358791, 242 | ], 243 | [ 244 | -90.0720528514032, 245 | 43.18960249893664, 246 | ], 247 | [ 248 | -90.01943830409088, 249 | 43.1616178388178, 250 | ], 251 | [ 252 | -89.78764171720462, 253 | 43.17253794990775, 254 | ], 255 | [ 256 | -89.733134033242, 257 | 43.13500642780507, 258 | ], 259 | [ 260 | -89.62895493962768, 261 | 43.0993527034699, 262 | ], 263 | [ 264 | -89.51887431562832, 265 | 43.09672812003931, 266 | ], 267 | [ 268 | -89.52737897618736, 269 | 43.05567196842142, 270 | ], 271 | [ 272 | -89.43746684127434, 273 | 43.0616327142744, 274 | ], 275 | [ 276 | -89.34661463842666, 277 | 43.09381691742976, 278 | ], 279 | [ 280 | -89.26123665654032, 281 | 43.07289141234296, 282 | ], 283 | [ 284 | -89.21686596245432, 285 | 43.03353707775756, 286 | ], 287 | [ 288 | -89.09576531507922, 289 | 43.03674219180181, 290 | ], 291 | [ 292 | -89.02172561743205, 293 | 43.00874839540851, 294 | ], 295 | [ 296 | -88.77995766707582, 297 | 43.004802363033406, 298 | ], 299 | [ 300 | -88.51334877320427, 301 | 43.02792220846101, 302 | ], 303 | [ 304 | -88.39452993132956, 305 | 43.011792367364286, 306 | ], 307 | [ 308 | -88.01441875294597, 309 | 43.01660827365053, 310 | ], 311 | [ 312 | -87.96884785414484, 313 | 43.0330065032868, 314 | ], 315 | [ 316 | -87.19450316400005, 317 | 43.10075575024178, 318 | ], 319 | [ 320 | -86.58349284910278, 321 | 43.17944539249239, 322 | ], 323 | [ 324 | -86.32540156141512, 325 | 43.23260082401636, 326 | ], 327 | [ 328 | -86.25997904696153, 329 | 43.219804340082426, 330 | ], 331 | [ 332 | -86.25460440300988, 333 | 43.23770368475768, 334 | ], 335 | [ 336 | -86.20335753083806, 337 | 43.26515072960711, 338 | ], 339 | [ 340 | -86.20268404491823, 341 | 43.29509486913988, 342 | ], 343 | [ 344 | -86.17055444986202, 345 | 43.31968188289862, 346 | ], 347 | [ 348 | -86.16736538716235, 349 | 43.35958405867532, 350 | ], 351 | [ 352 | -86.03919217371012, 353 | 43.431510929990026, 354 | ], 355 | [ 356 | -86.03962652393247, 357 | 43.56308099352155, 358 | ], 359 | [ 360 | -85.77583093204142, 361 | 43.568461840081554, 362 | ], 363 | [ 364 | -85.7712466177398, 365 | 43.597866729873786, 366 | ], 367 | [ 368 | -85.79884562649373, 369 | 43.60435302373357, 370 | ], 371 | [ 372 | -85.81580406062847, 373 | 43.64066858314317, 374 | ], 375 | [ 376 | -85.81884258434718, 377 | 43.72832271274526, 378 | ], 379 | [ 380 | -85.85014538847454, 381 | 43.76811089463883, 382 | ], 383 | [ 384 | -85.84292018794068, 385 | 44.05607394412537, 386 | ], 387 | [ 388 | -85.8010058105791, 389 | 44.14637671322016, 390 | ], 391 | [ 392 | -85.80291436993126, 393 | 44.21666764658337, 394 | ], 395 | [ 396 | -85.50964188769973, 397 | 44.21742293987816, 398 | ], 399 | [ 400 | -85.4038331844708, 401 | 44.23735644672461, 402 | ], 403 | [ 404 | -85.39509245202447, 405 | 44.45667640310196, 406 | ], 407 | [ 408 | -85.40578356951978, 409 | 44.50323351663025, 410 | ], 411 | [ 412 | -85.35294715323052, 413 | 44.586115451571146, 414 | ], 415 | [ 416 | -85.35337915651998, 417 | 44.64688533921228, 418 | ], 419 | [ 420 | -85.4049244244447, 421 | 44.683844209690285, 422 | ], 423 | [ 424 | -85.41120741524306, 425 | 44.88530077975698, 426 | ], 427 | [ 428 | -85.35308755010864, 429 | 44.98986816503572, 430 | ], 431 | [ 432 | -85.35938277848564, 433 | 45.097954189446845, 434 | ], 435 | [ 436 | -85.34317368649444, 437 | 45.263742199209766, 438 | ], 439 | [ 440 | -85.16914727174226, 441 | 45.36440965258085, 442 | ], 443 | [ 444 | -85.0305936579453, 445 | 45.35840967674307, 446 | ], 447 | [ 448 | -84.90665523343245, 449 | 45.39399735168125, 450 | ], 451 | [ 452 | -84.91326159805517, 453 | 45.42787566319613, 454 | ], 455 | [ 456 | -85.04987329252535, 457 | 45.447893911083646, 458 | ], 459 | [ 460 | -85.09555835284768, 461 | 45.49304843884473, 462 | ], 463 | [ 464 | -85.11305943139374, 465 | 45.58033112440778, 466 | ], 467 | [ 468 | -85.06557553083177, 469 | 45.63165925913084, 470 | ], 471 | [ 472 | -84.97421865359412, 473 | 45.671588801821386, 474 | ], 475 | [ 476 | -84.95163026273711, 477 | 45.70272958376045, 478 | ], 479 | [ 480 | -84.7871126980379, 481 | 45.69455701862061, 482 | ], 483 | [ 484 | -84.72804073252225, 485 | 45.733121107375, 486 | ], 487 | [ 488 | -84.72280405469678, 489 | 45.86758267604361, 490 | ], 491 | [ 492 | -84.727622559373, 493 | 45.87309004743199, 494 | ], 495 | [ 496 | -84.75159237169171, 497 | 45.96522480719347, 498 | ], 499 | [ 500 | -84.63777090430692, 501 | 46.10944316261085, 502 | ], 503 | [ 504 | -84.5659375750316, 505 | 46.14829630129612, 506 | ], 507 | [ 508 | -84.55897858374244, 509 | 46.23763686981633, 510 | ], 511 | [ 512 | -84.42900124945159, 513 | 46.33386321370149, 514 | ], 515 | [ 516 | -84.38266759746185, 517 | 46.4772227174104, 518 | ], 519 | [ 520 | -84.3455553800653, 521 | 46.534107551022, 522 | ], 523 | [ 524 | -84.31914576337144, 525 | 46.54528511226787, 526 | ], 527 | [ 528 | -84.32159076452658, 529 | 46.5753866639422, 530 | ], 531 | [ 532 | -84.323, 533 | 46.5753866639422, 534 | ], 535 | [ 536 | -84.324, 537 | 46.5753866639422, 538 | ], 539 | [ 540 | -84.325, 541 | 46.5753866639422, 542 | ], 543 | [ 544 | -84.327, 545 | 46.572, 546 | ], 547 | ], 548 | type: 'LineString', 549 | }, 550 | legs: [{ 551 | summary: '', 552 | weight: 3490.8, 553 | duration: 3490.8, 554 | steps: [], 555 | distance: 4827.1, 556 | }], 557 | weight_name: 'duration', 558 | weight: 3490.8, 559 | duration: 3490.8, 560 | distance: 4827.1, 561 | }], 562 | waypoints: [{ 563 | hint: '0qodgOKDNILw0AIASgUAAAAAAAAAAAAAUgAAAEoFAAAAAAAAAAAAAFIAAAACphYABwAAAIYPzQBFAiEDhg_NAEUCIQMAAAMDPkaMgQ==', 564 | name: 'Glogauer Straße', 565 | location: [13.438854, 52.494917], 566 | }, { 567 | hint: 'gNt0jZVKepEAAAAAMgEAAAAAAAAAAAAAAAAAADIBAAAAAAAAAAAAAAAAAAC7m5EHBwAAABhDzAAIViEDJEbMAARVIQMAAAMDPkaMgQ==', 568 | name: '', 569 | location: [13.38652, 52.51636], 570 | }], 571 | }; 572 | -------------------------------------------------------------------------------- /test/staticmaps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import StaticMaps from '../src/staticmaps'; 4 | import GeoJSON from './static/geojson'; 5 | import MultiPolygonGeometry from './static/multipolygonGeometry'; 6 | import Route from './static/routeLong'; 7 | 8 | const { expect } = require('chai'); 9 | 10 | const markerPath = path.join(__dirname, 'marker.png'); 11 | 12 | describe('StaticMap', () => { 13 | describe('Initializing ...', () => { 14 | it('without any arguments', () => { 15 | expect(() => { 16 | const options = { 17 | width: 600, 18 | height: 200, 19 | }; 20 | const map = new StaticMaps(options); 21 | expect(map.constructor.name).to.be.equal('StaticMaps'); 22 | }).to.not.throw(); 23 | }); 24 | }); 25 | 26 | describe('Rendering ...', () => { 27 | it('render w/ center', async () => { 28 | const options = { 29 | width: 600, 30 | height: 200, 31 | }; 32 | const map = new StaticMaps(options); 33 | await map.render([13.437524, 52.4945528], 13); 34 | await map.image.save('test/out/01-center.jpg'); 35 | }).timeout(0); 36 | 37 | it('render quadKeys based map', async () => { 38 | const options = { 39 | width: 600, 40 | height: 200, 41 | tileUrl: 'http://ak.dynamic.{s}.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=en-US&it=G,L&shading=hill&og=1757&n=z', 42 | tileSubdomains: ['t0', 't1', 't2', 't3'], 43 | }; 44 | const map = new StaticMaps(options); 45 | await map.render([13.437524, 52.4945528], 13); 46 | await map.image.save('test/out/01a-quadkeys.jpg'); 47 | }).timeout(0); 48 | 49 | it('render w/ center from custom', async () => { 50 | const options = { 51 | width: 600, 52 | height: 200, 53 | }; 54 | 55 | const map = new StaticMaps(options); 56 | await map.render([13.437524, 52.4945528], 13); 57 | await map.image.save('test/out/02-center_osm.png'); 58 | }).timeout(0); 59 | 60 | it('render w/ bbox', async () => { 61 | const options = { 62 | width: 600, 63 | height: 300, 64 | quality: 80, 65 | paddingY: 50, 66 | paddingX: 50, 67 | }; 68 | 69 | const map = new StaticMaps(options); 70 | await map.render([-6.1359285, 53.3145145, -6.1058408, 53.3253966]); 71 | await map.image.save('test/out/03-bbox.png'); 72 | }).timeout(0); 73 | 74 | it('render w/ marker', async () => { 75 | const options = { 76 | width: 500, 77 | height: 500, 78 | }; 79 | 80 | const map = new StaticMaps(options); 81 | 82 | const marker = { 83 | img: markerPath, 84 | // offsetX: 24, // default 85 | // offsetY: 48, // default 86 | width: 48, 87 | height: 48, 88 | }; 89 | 90 | marker.coord = [13.437524, 52.4945528]; 91 | map.addMarker(marker); 92 | 93 | marker.coord = [13.430524, 52.4995528]; 94 | map.addMarker(marker); 95 | 96 | await map.render([13.437524, 52.4945528], 12); 97 | await map.image.save('test/out/04-marker.png'); 98 | }).timeout(0); 99 | 100 | it('render w/ marker resized', async () => { 101 | const options = { 102 | width: 500, 103 | height: 500, 104 | }; 105 | 106 | const map = new StaticMaps(options); 107 | 108 | const marker = { 109 | img: markerPath, 110 | // offsetX: 12, 111 | // offsetY: 24, 112 | drawWidth: 24, 113 | drawHeight: 24, 114 | }; 115 | 116 | marker.coord = [13.437524, 52.4945528]; 117 | map.addMarker(marker); 118 | 119 | marker.coord = [13.430524, 52.4995528]; 120 | map.addMarker(marker); 121 | 122 | await map.render([13.437524, 52.4945528], 12); 123 | await map.image.save('test/out/04a-marker-resize.png'); 124 | }).timeout(0); 125 | 126 | it('render w/ remote url icon', async () => { 127 | const options = { 128 | width: 500, 129 | height: 500, 130 | }; 131 | 132 | const map = new StaticMaps(options); 133 | 134 | const marker = { 135 | img: 'https://img.icons8.com/color/48/000000/marker.png', 136 | offsetX: 24, 137 | offsetY: 48, 138 | width: 48, 139 | height: 48, 140 | }; 141 | 142 | marker.coord = [13.437524, 52.4945528]; 143 | map.addMarker(marker); 144 | 145 | marker.coord = [13.430524, 52.4995528]; 146 | map.addMarker(marker); 147 | 148 | await map.render([13.437524, 52.4945528], 12); 149 | await map.image.save('test/out/04-marker-remote.png'); 150 | }).timeout(0); 151 | 152 | it('render w/out center', async () => { 153 | const options = { 154 | width: 1200, 155 | height: 800, 156 | }; 157 | const map = new StaticMaps(options); 158 | const marker = { 159 | img: markerPath, 160 | offsetX: 24, 161 | offsetY: 48, 162 | width: 48, 163 | height: 48, 164 | }; 165 | 166 | marker.coord = [13.437524, 52.4945528]; 167 | map.addMarker(marker); 168 | marker.coord = [13.430524, 52.4995528]; 169 | map.addMarker(marker); 170 | marker.coord = [13.410524, 52.5195528]; 171 | map.addMarker(marker); 172 | 173 | await map.render(); 174 | await map.image.save('test/out/05-marker-nocenter.png'); 175 | }).timeout(0); 176 | 177 | it('render w/out base layer', async () => { 178 | const options = { 179 | width: 800, 180 | height: 800, 181 | paddingX: 0, 182 | paddingY: 0, 183 | quality: 10, 184 | tileUrl: undefined, 185 | }; 186 | const map = new StaticMaps(options); 187 | 188 | const coords = Route.routes[0].geometry.coordinates; 189 | 190 | const marker = { 191 | img: markerPath, 192 | offsetX: 24, 193 | offsetY: 48, 194 | width: 48, 195 | height: 48, 196 | }; 197 | [marker.coord] = coords; 198 | map.addMarker(marker); 199 | marker.coord = coords[coords.length - 1]; 200 | map.addMarker(marker); 201 | 202 | const polyline = { 203 | coords, 204 | color: '#0000FF66', 205 | width: 3, 206 | }; 207 | map.addLine(polyline); 208 | 209 | const text = { 210 | coord: coords[Math.round(coords.length / 2)], 211 | offsetX: 100, 212 | offsetY: 50, 213 | text: 'TEXT', 214 | size: 50, 215 | width: '1px', 216 | fill: '#000000', 217 | color: '#ffffff', 218 | font: 'Impact', 219 | anchor: 'middle', 220 | }; 221 | 222 | map.addText(text); 223 | 224 | await map.render(); 225 | await map.image.save('test/out/05-annotations-nobaselayer.png'); 226 | }).timeout(0); 227 | }); 228 | 229 | describe('Rendering w/ polylines ...', () => { 230 | it('Render StaticMap w/ single polyline', async () => { 231 | const options = { 232 | width: 800, 233 | height: 800, 234 | paddingX: 0, 235 | paddingY: 0, 236 | quality: 10, 237 | }; 238 | 239 | const map = new StaticMaps(options); 240 | 241 | const coords = Route.routes[0].geometry.coordinates; 242 | const polyline = { 243 | coords, 244 | color: '#0000FF66', 245 | width: 3, 246 | }; 247 | 248 | const polyline2 = { 249 | coords, 250 | color: '#FFFFFF00', 251 | width: 6, 252 | }; 253 | 254 | map.addLine(polyline2); 255 | map.addLine(polyline); 256 | await map.render(); 257 | await map.image.save('test/out/06-polyline.jpg'); 258 | }).timeout(0); 259 | }); 260 | 261 | describe('Rendering w/ polygons ...', () => { 262 | it('Render StaticMap w/ polygon', async () => { 263 | const options = { 264 | width: 600, 265 | height: 300, 266 | paddingX: 50, 267 | paddingY: 50, 268 | }; 269 | 270 | const map = new StaticMaps(options); 271 | 272 | const polygon = { 273 | coords: GeoJSON.way.geometry.coordinates[0][0], 274 | color: '#0000FFBB', 275 | fill: '#000000BB', 276 | width: 1, 277 | }; 278 | 279 | map.addPolygon(polygon); 280 | await map.render(); 281 | await map.image.save('test/out/07-polygon.png'); 282 | }).timeout(0); 283 | 284 | it('Render StaticMap w/ multipolygon', async () => { 285 | const options = { 286 | width: 600, 287 | height: 300, 288 | paddingX: 80, 289 | paddingY: 80, 290 | }; 291 | 292 | const map = new StaticMaps(options); 293 | 294 | const multipolygon = { 295 | coords: MultiPolygonGeometry.geometry.coordinates, 296 | color: '#0000FFBB', 297 | fill: '#000000BB', 298 | width: 1, 299 | }; 300 | 301 | map.addMultiPolygon(multipolygon); 302 | await map.render(); 303 | await map.image.save('test/out/07-multipolygon.png'); 304 | }).timeout(0); 305 | 306 | it('Render StaticMap w/ thousands of polygons', async () => { 307 | const options = { 308 | width: 600, 309 | height: 300, 310 | paddingX: 50, 311 | paddingY: 50, 312 | }; 313 | 314 | const map = new StaticMaps(options); 315 | 316 | const polygon = { 317 | coords: GeoJSON.way.geometry.coordinates[0][0], 318 | color: '#0000FFBB', 319 | fill: '#000000BB', 320 | width: 1, 321 | }; 322 | 323 | for (let i = 0; i < 10000; i++) map.addPolygon({ ...polygon }); 324 | await map.render([13.437524, 52.4945528], 13); 325 | await map.image.save('test/out/07-multiple-polygons.png'); 326 | }).timeout(0); 327 | }); 328 | 329 | describe('Rendering circles ...', () => { 330 | it('Render StaticMap w/ circle', async () => { 331 | const options = { 332 | width: 600, 333 | height: 300, 334 | paddingX: 20, 335 | paddingY: 20, 336 | }; 337 | 338 | const map = new StaticMaps(options); 339 | 340 | const circle = { 341 | coord: [13.01, 51.98], 342 | radius: 500, 343 | fill: '#000000', 344 | width: 0, 345 | }; 346 | 347 | const circle2 = { 348 | coord: [13.01, 51.98], 349 | radius: 800, 350 | fill: '#fab700CC', 351 | color: '#FFFFFF', 352 | width: 2, 353 | }; 354 | 355 | map.addCircle(circle2); 356 | map.addCircle(circle); 357 | await map.render(); 358 | await map.image.save('test/out/099-circle.png'); 359 | }).timeout(0); 360 | }); 361 | 362 | describe('Rendering text ...', () => { 363 | it('Render StaticMap with text', async () => { 364 | const options = { 365 | width: 1200, 366 | height: 800, 367 | }; 368 | 369 | const map = new StaticMaps(options); 370 | const text = { 371 | coord: [13.437524, 52.4945528], 372 | offsetX: 100, 373 | offsetY: 50, 374 | text: 'TEXT', 375 | size: 50, 376 | width: '1px', 377 | fill: '#000000', 378 | color: '#ffffff', 379 | font: 'Impact', 380 | anchor: 'middle', 381 | }; 382 | 383 | map.addText(text); 384 | 385 | await map.render([13.437524, 52.4945528]); 386 | await map.image.save('test/out/08-text-center.png'); 387 | }).timeout(0); 388 | 389 | it('Render text on NASA Blue Marble', async () => { 390 | const options = { 391 | width: 1200, 392 | height: 800, 393 | tileUrl: 'https://map1.vis.earthdata.nasa.gov/wmts-webmerc/BlueMarble_NextGeneration/default/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpg', 394 | zoomRange: { 395 | max: 8, 396 | }, 397 | }; 398 | 399 | const map = new StaticMaps(options); 400 | const text = { 401 | coord: [13.437524, 52.4945528], 402 | text: 'My Text', 403 | size: 50, 404 | width: '1px', 405 | fill: '#000000', 406 | color: '#ffffff', 407 | font: 'Calibri', 408 | }; 409 | 410 | map.addText(text); 411 | 412 | await map.render([13.437524, 52.4945528]); 413 | await map.image.save('test/out/09-text-nasabm.png'); 414 | }).timeout(0); 415 | }); 416 | 417 | describe('Rendering buffer ...', () => { 418 | it('render w/ center', async () => { 419 | const options = { 420 | width: 600, 421 | height: 200, 422 | }; 423 | 424 | const map = new StaticMaps(options); 425 | await map.render([13.437524, 52.4945528], 13); 426 | await map.image.buffer('image/png'); 427 | }).timeout(0); 428 | }); 429 | 430 | describe('Fetch tiles from subdomains', () => { 431 | it('should fetch from subdomains', async () => { 432 | const options = { 433 | width: 1024, 434 | height: 1024, 435 | tileSubdomains: ['a', 'b', 'c'], 436 | tileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 437 | }; 438 | 439 | const map = new StaticMaps(options); 440 | 441 | await map.render([13.437524, 52.4945528], 13); 442 | await map.image.save('test/out/10-subdomains.png'); 443 | }).timeout(0); 444 | }); 445 | 446 | describe('Fetch tiles from multiple layers', () => { 447 | it('should assemble layers', async () => { 448 | const options = { 449 | width: 1024, 450 | height: 600, 451 | tileLayers: [{ 452 | tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', 453 | }, { 454 | tileUrl: 'http://www.openfiremap.de/hytiles/{z}/{x}/{y}.png', 455 | }], 456 | }; 457 | 458 | const map = new StaticMaps(options); 459 | 460 | await map.render([13.437524, 52.4945528], 13); 461 | await map.image.save('test/out/11-layers.png'); 462 | }).timeout(0); 463 | }); 464 | }); 465 | -------------------------------------------------------------------------------- /test/unit/circle.js: -------------------------------------------------------------------------------- 1 | import Circle from '../../src/circle'; 2 | 3 | const { expect } = require('chai'); 4 | 5 | 6 | describe('StaticMap', () => { 7 | describe('Circle ...', () => { 8 | it('without any arguments', () => { 9 | const circle = new Circle({ coord: [13, 52], radius: 1000 }); 10 | const ext = circle.extent(); 11 | console.log({ ext }); 12 | /* expect(() => { 13 | const options = { 14 | width: 600, 15 | height: 200, 16 | }; 17 | const map = new StaticMaps(options); 18 | expect(map.constructor.name).to.be.equal('StaticMaps'); 19 | }).to.not.throw(); */ 20 | }); 21 | }); 22 | }); 23 | --------------------------------------------------------------------------------