├── .travis.yml
├── lib
├── abaculus.js
├── capabilities.js
├── checkTile.js
├── find.js
├── getEtag.js
├── image-type.js
├── index.js
├── makeError.js
├── makeHost.js
├── makeUnknownError.js
├── map.js
├── memCache.js
├── merc.js
├── normalizeObj.js
├── promise.js
└── tile.js
├── license.md
├── package-lock.json
├── package.json
├── readme.md
├── templates
├── wms.hbs
└── wmts.hbs
└── test
└── basic.js
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - '12'
5 | - '14'
6 | - '16'
7 |
8 | addons:
9 | apt:
10 | sources:
11 | - ubuntu-toolchain-r-test
12 | packages:
13 | - libstdc++-4.9-dev
14 |
15 | before_script:
16 | - npm install
17 |
18 | script:
19 | - npm test
20 |
--------------------------------------------------------------------------------
/lib/abaculus.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2016, Mapbox
3 |
4 | Permission to use, copy, modify, and/or distribute this software for any
5 | purpose with or without fee is hereby granted, provided that the above
6 | copyright notice and this permission notice appear in all copies.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 | */
16 |
17 |
18 | var SphericalMercator = require('@mapbox/sphericalmercator');
19 | var queue = require('d3-queue').queue;
20 | var blend = require('mapnik').blend;
21 | var crypto = require('crypto');
22 |
23 | module.exports = abaculus;
24 |
25 | function abaculus(arg, callback) {
26 | var z = arg.zoom || 0,
27 | s = arg.scale || 1,
28 | center = arg.center || null,
29 | bbox = arg.bbox || null,
30 | getTile = arg.getTile || null,
31 | format = arg.format || 'png',
32 | quality = arg.quality || undefined,
33 | limit = arg.limit || 19008,
34 | tileSize = arg.tileSize || 256;
35 |
36 | if (!getTile) return callback(new Error('Invalid function for getting tiles'));
37 |
38 | if (center) {
39 | // get center coordinates in px from lng,lat
40 | center = abaculus.coordsFromCenter(z, s, center, limit, tileSize);
41 | } else if (bbox) {
42 | // get center coordinates in px from [w,s,e,n] bbox
43 | center = abaculus.coordsFromBbox(z, s, bbox, limit, tileSize);
44 | } else {
45 | return callback(new Error('No coordinates provided.'));
46 | }
47 | // generate list of tile coordinates center
48 | var coords = abaculus.tileList(z, s, center, tileSize);
49 |
50 | // get tiles based on coordinate list and stitch them together
51 | abaculus.stitchTiles(coords, format, quality, getTile, callback);
52 | }
53 |
54 | abaculus.coordsFromBbox = function(z, s, bbox, limit, tileSize) {
55 | var sm = new SphericalMercator({ size: tileSize * s });
56 | var topRight = sm.px([bbox[2], bbox[3]], z),
57 | bottomLeft = sm.px([bbox[0], bbox[1]], z);
58 | var center = {};
59 | center.w = topRight[0] - bottomLeft[0];
60 | center.h = bottomLeft[1] - topRight[1];
61 |
62 | if (center.w <= 0 || center.h <= 0) throw new Error('Incorrect coordinates');
63 |
64 | var origin = [topRight[0] - center.w / 2, topRight[1] + center.h / 2];
65 | center.x = origin[0];
66 | center.y = origin[1];
67 | center.w = Math.round(center.w * s);
68 | center.h = Math.round(center.h * s);
69 |
70 | if (center.w >= limit || center.h >= limit) throw new Error('Desired image is too large.');
71 | return center;
72 | };
73 |
74 | abaculus.coordsFromCenter = function(z, s, center, limit, tileSize) {
75 | var sm = new SphericalMercator({ size: tileSize * s });
76 | var origin = sm.px([center.x, center.y], z);
77 | center.x = origin[0];
78 | center.y = origin[1];
79 | center.w = Math.round(center.w * s);
80 | center.h = Math.round(center.h * s);
81 |
82 | if (center.w >= limit || center.h >= limit) throw new Error('Desired image is too large.');
83 | return center;
84 | };
85 |
86 | // Generate the zxy and px/py offsets needed for each tile in a static image.
87 | // x, y are center coordinates in pixels
88 | abaculus.tileList = function(z, s, center, tileSize) {
89 | var x = center.x,
90 | y = center.y,
91 | w = center.w,
92 | h = center.h;
93 | var dimensions = {x: w - 1, y: h - 1};
94 | var size = tileSize || 256;
95 | var ts = Math.floor(size * s);
96 |
97 | var centerCoordinate = {
98 | column: x / size,
99 | row: y / size,
100 | zoom: z
101 | };
102 |
103 | function pointCoordinate(point) {
104 | var coord = {
105 | column: centerCoordinate.column,
106 | row: centerCoordinate.row,
107 | zoom: centerCoordinate.zoom,
108 | };
109 | coord.column += (point.x - w / 2) / ts;
110 | coord.row += (point.y - h / 2) / ts;
111 | return coord;
112 | }
113 |
114 | function coordinatePoint(coord) {
115 | // Return an x, y point on the map image for a given coordinate.
116 | if (coord.zoom != z) coord = coord.zoomTo(z);
117 | return {
118 | x: w / 2 + ts * (coord.column - centerCoordinate.column),
119 | y: h / 2 + ts * (coord.row - centerCoordinate.row)
120 | };
121 | }
122 |
123 | function floorObj(obj) {
124 | return {
125 | row: Math.floor(obj.row),
126 | column: Math.floor(obj.column),
127 | zoom: obj.zoom
128 | };
129 | }
130 |
131 | var maxTilesInRow = Math.pow(2, z);
132 | var tl = floorObj(pointCoordinate({x: 0, y:0}));
133 | var br = floorObj(pointCoordinate(dimensions));
134 | var coords = {};
135 | coords.tiles = [];
136 |
137 | for (var column = tl.column; column <= br.column; column++) {
138 | for (var row = tl.row; row <= br.row; row++) {
139 | var c = {
140 | column: column,
141 | row: row,
142 | zoom: z,
143 | };
144 | var p = coordinatePoint(c);
145 |
146 | // Wrap tiles with negative coordinates.
147 | c.column = c.column % maxTilesInRow;
148 | if (c.column < 0) c.column = maxTilesInRow + c.column;
149 |
150 | if (c.row < 0 || c.row >= maxTilesInRow) continue;
151 | coords.tiles.push({
152 | z: c.zoom,
153 | x: c.column,
154 | y: c.row,
155 | px: Math.round(p.x),
156 | py: Math.round(p.y)
157 | });
158 | }
159 | }
160 | coords.dimensions = { x: w, y: h };
161 | coords.center = floorObj(centerCoordinate);
162 | coords.scale = s;
163 |
164 | return coords;
165 | };
166 |
167 | abaculus.stitchTiles = function(coords, format, quality, getTile, callback) {
168 | if (!coords) return callback(new Error('No coords object.'));
169 | var tileQueue = queue(32);
170 | var w = coords.dimensions.x,
171 | h = coords.dimensions.y,
172 | s = coords.scale,
173 | tiles = coords.tiles;
174 |
175 | tiles.forEach(function(t) {
176 | tileQueue.defer(function(z, x, y, px, py, done) {
177 | var cb = function(err, buffer, headers) {
178 | if (err) return done(err);
179 | done(err, {
180 | buffer: buffer,
181 | headers: headers,
182 | x: px,
183 | y: py,
184 | reencode: true
185 | })
186 | };
187 | cb.scale = s;
188 | cb.format = format;
189 | // getTile is a function that returns
190 | // a tile given z, x, y, & callback
191 | getTile(z, x, y, cb);
192 | }, t.z, t.x, t.y, t.px, t.py);
193 | });
194 |
195 | function tileQueueFinish(err, data) {
196 | if (err) return callback(err);
197 | if (!data) return callback(new Error('No tiles to stitch.'));
198 | var headers = [];
199 | data.forEach(function(d) {
200 | headers.push(d.headers);
201 | });
202 | var opts = {
203 | format: format,
204 | width: w,
205 | height: h,
206 | reencode: true
207 | }
208 | if (quality) {
209 | opts.quality = quality;
210 | }
211 | blend(data, opts, function(err, buffer) {
212 | if (err) return callback(err);
213 | callback(null, buffer, headerReduce(headers, format));
214 | });
215 | }
216 |
217 | tileQueue.awaitAll(tileQueueFinish);
218 | };
219 |
220 | // Calculate TTL from newest (max mtime) layer.
221 | function headerReduce(headers, format) {
222 | var minmtime = new Date('Sun, 23 Feb 2014 18:00:00 UTC');
223 | var composed = {};
224 |
225 | composed['Cache-Control'] = 'max-age=3600';
226 |
227 | switch (format) {
228 | case 'vector.pbf':
229 | composed['Content-Type'] = 'application/x-protobuf';
230 | composed['Content-Encoding'] = 'deflate';
231 | break;
232 | case 'jpeg':
233 | composed['Content-Type'] = 'image/jpeg';
234 | break;
235 | case 'png':
236 | composed['Content-Type'] = 'image/png';
237 | break;
238 | }
239 |
240 | var times = headers.reduce(function(memo, h) {
241 | if (!h) return memo;
242 | for (var k in h) if (k.toLowerCase() === 'last-modified') {
243 | memo.push(new Date(h[k]));
244 | return memo;
245 | }
246 | return memo;
247 | }, []);
248 | if (!times.length) {
249 | times.push(new Date());
250 | } else {
251 | times.push(minmtime);
252 | }
253 | composed['Last-Modified'] = (new Date(Math.max.apply(Math, times))).toUTCString();
254 |
255 | var etag = headers.reduce(function(memo, h) {
256 | if (!h) return memo;
257 | for (var k in h) if (k.toLowerCase() === 'etag') {
258 | memo.push(h[k]);
259 | return memo;
260 | }
261 | return memo;
262 | }, []);
263 | if (!etag.length) {
264 | composed['ETag'] = '"' + crypto.createHash('md5').update(composed['Last-Modified']).digest('hex') + '"';
265 | } else {
266 | composed['ETag'] = etag.length === 1 ? etag[0] : '"' + crypto.createHash('md5').update(etag.join(',')).digest('hex') + '"';
267 | }
268 |
269 | return composed;
270 | }
271 |
--------------------------------------------------------------------------------
/lib/capabilities.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Promise = require('./promise');
3 | var fs = Promise.promisifyAll(require('fs'));
4 | var handlebars = require('handlebars');
5 | var path = require('path');
6 | var getEtag = require('./getEtag');
7 | var wms = fs.readFileAsync(path.join(__dirname, '../templates/wms.hbs')).then(function (template) {
8 | return handlebars.compile(template.toString());
9 | });
10 | var wmts = fs.readFileAsync(path.join(__dirname, '../templates/wmts.hbs')).then(function (template) {
11 | return handlebars.compile(template.toString());
12 | });
13 | var merc = require('./merc');
14 | var normalizeObj = require('./normalizeObj');
15 | var defaultCache = require('./memCache');
16 |
17 | var makeUnknownError = require('./makeUnknownError');
18 | var getCapabilities = Promise.coroutine(_getCapabilities);
19 | function filterAbstract (abstract) {
20 | if (!abstract) {
21 | return '';
22 | }
23 | if (abstract.indexOf(']]>') > -1) {
24 | return 'Abstract Contains invalid sequences.';
25 | }
26 | return abstract;
27 | }
28 | var makeHost = require('./makeHost');
29 | module.exports = module.exports = function (layers, params, cache, callback) {
30 | if (typeof cache === 'function') {
31 | callback = cache;
32 | cache = undefined;
33 | }
34 | if (typeof cache === 'undefined') {
35 | cache = defaultCache;
36 | }
37 | return getCapabilities(layers, params, cache).asCallback(callback);
38 | };
39 |
40 | function * _getCapabilities(layers, rawParams) {
41 | try {
42 | layers = yield layers;
43 | } catch (e) {
44 | return makeUnknownError(e);
45 | }
46 | if (!layers) {
47 | throw new TypeError('options are required');
48 | }
49 | var params = normalizeObj(rawParams);
50 | var template;
51 | var service = params.service && params.service.toLowerCase();
52 | if (service === 'wms') {
53 | template = yield wms;
54 | } else if (service === 'wmts') {
55 | template = yield wmts;
56 | } else {
57 | throw new TypeError(`unknown service: ${params.service}`);
58 | }
59 | var tileSets = new Set();
60 | var out = {
61 | title: layers.title || '',
62 | abstract: filterAbstract(layers.abstract),
63 | host: makeHost(layers.host),
64 | layers: makeLayers(layers.layers, tileSets),
65 | tilematrix: []
66 | };
67 | if (typeof layers.layerLimit === 'number') {
68 | out.layerLimit = layers.layerLimit;
69 | }
70 | if (layers.wmshost) {
71 | out.wmshost = makeHost(layers.wmshost);
72 | } else {
73 | out.wmshost = out.host;
74 | }
75 | for (let matrix of tileSets) {
76 | out.tilematrix.push(makeTileSet(matrix));
77 | }
78 | var data = Buffer.from(template(out));
79 | var headers = {
80 | 'content-type': 'text/xml',
81 | 'content-length': data.length,
82 | etag: getEtag(data)
83 | };
84 | return {
85 | data: data,
86 | headers: headers,
87 | code: 200
88 | };
89 | }
90 |
91 | function makeLayers(layers, tileSets) {
92 | if (!Array.isArray(layers) || !layers.length) {
93 | throw new Error('layers are not optional');
94 | }
95 |
96 | return layers.map(function (layer) {
97 | if (!layer) {
98 | return;
99 | }
100 | if (layer.viewable === false) {
101 | return;
102 | }
103 | var out = {
104 | title: layer.title || '',
105 | name: layer.name || layer.identifier || '',
106 | abstract: filterAbstract(layer.abstract),
107 | png: true,
108 | jpeg: true
109 | };
110 |
111 | if (Array.isArray(layer.image)) {
112 | if (layer.image.indexOf('png') === -1) {
113 | out.png = false;
114 | }
115 | if (layer.image.indexOf('jpeg') === -1) {
116 | out.jpeg = false;
117 | }
118 | }
119 |
120 | var bl = merc.forward(layer.bbox.slice(0, 2));
121 | out.mercminx = bl[0];
122 | out.mercminy = bl[1];
123 | var tr = merc.forward(layer.bbox.slice(2, 4));
124 | out.mercmaxx = tr[0];
125 | out.mercmaxy = tr[1];
126 | out.minx = layer.bbox[0];
127 | out.miny = layer.bbox[1];
128 | out.maxx = layer.bbox[2];
129 | out.maxy = layer.bbox[3];
130 | out.tileset = layer.range.join('to');
131 | tileSets.add(out.tileset);
132 | out.tilelimits = getTileLimits(layer.bbox, layer.range[0], layer.range[1]);
133 | return out;
134 | }).filter(function (layer) {
135 | return layer;
136 | });
137 | }
138 | function getTileLimits(bbox, minzoom, maxzoom) {
139 | var i = minzoom;
140 | var out = [];
141 | while (i <= maxzoom) {
142 | out.push({
143 | zoom: i < 10 ? '0' + i.toString() : i.toString(),
144 | bbox: merc.xyz(bbox, i)
145 | });
146 | i++;
147 | }
148 | return out;
149 | }
150 | function makeTileSet(layer) {
151 | var out = {name: layer};
152 | var split = layer.split('to');
153 | var min = parseInt(split[0], 10);
154 | var max = parseInt(split[1], 10);
155 | var i = min;
156 | while (i <= max) {
157 | out['level' + i] = true;
158 | i++;
159 | }
160 | return out;
161 | }
162 |
--------------------------------------------------------------------------------
/lib/checkTile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var merc = require('./merc');
3 |
4 | module.exports = checkTile;
5 | function checkTile(layer, x, y, z) {
6 | if (z < layer.range[0] || z > layer.range[1]) {
7 | return 'TILEMATRIX';
8 | }
9 | var tilebbox = merc.xyz(layer.bbox, z);
10 | if (y < tilebbox.minY || y > tilebbox.maxY) {
11 | return 'TILEROW';
12 | }
13 | if (x < tilebbox.minX || x > tilebbox.maxX) {
14 | return 'TILECOLUMN';
15 | }
16 | return false;
17 | }
18 |
--------------------------------------------------------------------------------
/lib/find.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = find;
4 |
5 | function find(layers, target) {
6 | if (!Array.isArray(layers)) {
7 | if (layers.abstract && Array.isArray(layers.layers)) {
8 | layers = layers.layers;
9 | } else if (layers.name === target || layers.identifier === target) {
10 | return normalize(layers);
11 | }
12 | }
13 | for (let layer of layers) {
14 | if (layer.name === target || layer.identifier === target) {
15 | return normalize(layer);
16 | }
17 | }
18 | }
19 | function normalize(layer) {
20 | if (!layer.name && layer.identifier) {
21 | layer.name = layer.identifier;
22 | }
23 | return layer;
24 | }
25 |
--------------------------------------------------------------------------------
/lib/getEtag.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var crypto = require('crypto');
4 |
5 | module.exports = getEtag;
6 |
7 | function getEtag(data) {
8 | return crypto.createHash('sha224').update(data).digest('base64');
9 | }
10 |
--------------------------------------------------------------------------------
/lib/image-type.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 | module.exports = imageType;
4 | function imageType(data) {
5 | if (data.slice(0, 4).toString('hex') === '89504e47') {
6 | return 'image/png';
7 | } else if (data.slice(0, 2).toString('hex') === 'ffd8') {
8 | return 'image/jpeg';
9 | }
10 | throw new Error('unknown image type');
11 | }
12 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var normalizeObj = require('./normalizeObj');
3 | var Promise = require('./promise');
4 | module.exports = exports = wms;
5 | exports.getCapabilities = require('./capabilities');
6 | exports.getTile = require('./tile');
7 | exports.getMap = require('./map');
8 |
9 | function wms(options, rawParams, cache, callback) {
10 | var params = normalizeObj(rawParams);
11 | if (typeof params.request !== 'string') {
12 | return Promise.resolve({
13 | code: 400,
14 | data: 'unknown/invalid request'
15 | }).asCallback(callback);
16 | }
17 | switch (params.request.toLowerCase()) {
18 | case 'getcapabilities':
19 | return exports.getCapabilities(options, params, cache, callback);
20 | case 'getmap':
21 | return exports.getMap(options, params, cache, callback);
22 | case 'gettile':
23 | return exports.getTile(options, params, cache, callback);
24 | default:
25 | return Promise.resolve({
26 | code: 400,
27 | data: 'unknown/invalid request'
28 | }).asCallback(callback);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/makeError.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var getEtag = require('./getEtag');
3 |
4 | var errors = new Map();
5 |
6 | var handlebars = require('handlebars');
7 | module.exports = makeError;
8 | function makeError(text, code, locator) {
9 | var key = [text, code, locator].join('/');
10 | var data;
11 | if (errors.has(key)) {
12 | data = errors.get(key);
13 | } else {
14 | var params = {
15 | code: code,
16 | text: text
17 | };
18 | if (locator) {
19 | params.locator = locator;
20 | }
21 | var errData = Buffer.from(rangeError(params), 'utf8');
22 | data = {
23 | resp: errData,
24 | etag: getEtag(errData),
25 | len: errData.length
26 | };
27 | errors.set(key, data);
28 | }
29 | return {
30 | data: data.resp,
31 | headers: {
32 | 'content-type': 'application/xml',
33 | 'content-length': data.len,
34 | etag: data.etag
35 | },
36 | code: 400
37 | };
38 | }
39 |
40 | var rangeError = handlebars.compile(`
43 |
44 | {{text}}
45 |
46 |
47 | `);
48 |
--------------------------------------------------------------------------------
/lib/makeHost.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var url = require('url');
3 |
4 | module.exports = makeHost;
5 |
6 | function makeHost(rawUrl) {
7 | if (!rawUrl) {
8 | throw new TypeError('no url provided');
9 | }
10 | var parsedUrl = url.parse(rawUrl);
11 | if (!parsedUrl.protocol) {
12 | throw new Error('invalid protocol');
13 | }
14 | var outUrl = `${parsedUrl.protocol}//`;
15 | if (parsedUrl.port === '80') {
16 | outUrl += parsedUrl.hostName;
17 | } else {
18 | outUrl += parsedUrl.host;
19 | }
20 | if (parsedUrl.pathname && parsedUrl.pathname !== '/') {
21 | var path = parsedUrl.pathname;
22 | if (path.slice(-1) === '/') {
23 | path = path.slice(0, -1);
24 | }
25 | outUrl += path;
26 | }
27 | return outUrl;
28 | }
29 |
--------------------------------------------------------------------------------
/lib/makeUnknownError.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var makeError = require('./makeError');
3 | module.exports = makeUnknownError;
4 | function makeUnknownError(e) {
5 | var message = typeof e === 'string' ? e : 'unknown error';
6 | if (process.env.NODE_ENV !== 'production' && e) {
7 | if (e.stack) {
8 | message = e.stack;
9 | } else if (e.message) {
10 | message = e.message;
11 | }
12 | }
13 | var resp = makeError(message, 'NoApplicableCode');
14 | resp.code = 500;
15 | return resp;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/map.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Promise = require('./promise');
4 | var normalizeObj = require('./normalizeObj');
5 | var makeError = require('./makeError');
6 | var checkTile = require('./checkTile');
7 | var merc = require('./merc');
8 | var mime = require('mime');
9 | var mapnik = require('mapnik');
10 | var Image = mapnik.Image;
11 | var blend = mapnik.blend;
12 | var Color = mapnik.Color;
13 | var defaultCache = require('./memCache');
14 | var EE = require('events').EventEmitter;
15 | var util = require('util')
16 | var makeUnknownError = require('./makeUnknownError');
17 | var _abaculus = require('./abaculus');
18 | var abaculus = util.promisify(_abaculus);
19 | var getEtag = require('./getEtag');
20 | var find = require('./find');
21 | var imageType = require('./image-type');
22 |
23 | var requiredParams = [
24 | 'layers',
25 | 'bbox',
26 | 'width',
27 | 'height',
28 | 'format',
29 | 'srs'
30 | ];
31 | const MAX_SIZE4 = 19008;
32 | const MAX_SIZE = 4752
33 | var fromBytes = util.promisify(Image.fromBytes);
34 |
35 | function mapnikResize(img, width, height) {
36 | return new Promise((yes, no) => {
37 | img.resize(width, height, {
38 | scaling_method: mapnik.imageScaling.sinc
39 | }, (err, resp) => {
40 | if (err) {
41 | no(err);
42 | } else {
43 | yes(resp);
44 | }
45 | });
46 | });
47 | }
48 |
49 | function mapnikEncode(img, format) {
50 | return new Promise((yes, no) => {
51 | img.encode(format, (err, resp) => {
52 | if (err) {
53 | no(err);
54 | } else {
55 | yes(resp);
56 | }
57 | });
58 | });
59 | }
60 |
61 | function maybePremultiply(img) {
62 | if (img.premultiplied()) {
63 | return Promise.resolve(img);
64 | }
65 | return new Promise((yes, no) => {
66 | img.premultiply((err, buf) => {
67 | if (err) {
68 | return no(err);
69 | }
70 | yes(buf);
71 | });
72 | });
73 | }
74 |
75 | async function resize(image, format, width, height, oWidth, oHeight) {
76 | image = Array.isArray(image) ? image[0] : image;
77 | if (width === oWidth && height === oHeight) {
78 | return image;
79 | }
80 | let mapnikImage = await fromBytes(image, {max_size: MAX_SIZE});
81 | let maybePremultiplied = await maybePremultiply(mapnikImage);
82 | let resized = await mapnikResize(maybePremultiplied, width, height);
83 | let output = await mapnikEncode(resized, format);
84 | return output;
85 | }
86 |
87 | async function getMap(layer, rawParams) {
88 | try {
89 | layer = await layer;
90 | } catch (e) {
91 | return makeUnknownError(e);
92 | }
93 | if (!layer) {
94 | throw new TypeError('must include layer');
95 | }
96 | var params = normalizeObj(rawParams);
97 | var i = -1;
98 | var len = requiredParams.length;
99 | var param;
100 | if (params.crs && !params.srs) {
101 | params.srs = params.crs;
102 | }
103 | while (++i < len) {
104 | param = requiredParams[i];
105 | if (!params[param]) {
106 | return makeError(`missing required parameter: ${param}`, 'MissingParameterValue', param);
107 | }
108 | }
109 | var srs = params.srs || params.crs;
110 | srs = srs.toLowerCase();
111 | if (['epsg:900913', 'epsg:3857', 'epsg:4326'].indexOf(srs) === -1) {
112 | return makeError(`invalid srs: ${srs}`, 'InvalidSRS');
113 | }
114 | var scale;
115 | if (params.dpi || params.map_resolution) {
116 | scale = Math.round(72 / parseInt(params.dpi || params.map_resolution, 10)) || 1;
117 | } else {
118 | scale = 1;
119 | }
120 | if (scale !== scale) {
121 | scale = 1;
122 | }
123 | var format = mime.extension(params.format);
124 | var bgcolor = params.bgcolor || '0xffffff';
125 | bgcolor = bgcolor.toLowerCase();
126 | if (params.transparent && params.transparent.toLowerCase() === 'true' && format === 'png') {
127 | bgcolor = false;
128 | }
129 | var abort = layer.abort || new EE();
130 | try {
131 | layer = makeLayer(layer, params.layers, bgcolor, format, abort);
132 | } catch (e) {
133 | if (e.layerLengthError) {
134 | return makeError(e.message, 'InvalidParameterValue', 'LAYER');
135 | }
136 | return makeError(e.message);
137 | }
138 | if (!layer) {
139 | return makeError(`No such layer: ${params.layer}`, 'InvalidParameterValue', 'LAYER');
140 | }
141 | if (layer.viewable === false) {
142 | return {
143 | headers: {},
144 | data: Buffer.from('not authorized'),
145 | code: 401
146 | };
147 | }
148 | if (typeof layer.getTile !== 'function') {
149 | throw new TypeError('must include getTile function');
150 | }
151 | var bbox = params.bbox.split(',').map(function(num) {
152 | return parseFloat(num);
153 | });
154 | if (['epsg:900913', 'epsg:3857'].indexOf(srs) > -1) {
155 | bbox = merc.inverse([bbox[0], bbox[1]]).concat(merc.inverse([bbox[2], bbox[3]]));
156 | }
157 | var width = parseInt(params.width, 10);
158 | if (width < 1) {
159 | return makeError('width must be greater then 0', 'InvalidParameterValue', 'WIDTH');
160 | }
161 | var height = parseInt(params.height, 10);
162 | if (height < 1) {
163 | return makeError('height must be greater then 1', 'InvalidParameterValue', 'HEIGHT');
164 | }
165 | var zoom = getZoom(bbox, [width, height], layer.range[0], layer.range[1]);
166 |
167 | var opts = {
168 | scale: scale,
169 | zoom: zoom,
170 | bbox: bbox,
171 | getTile: makeGetTile(layer, bgcolor, format),
172 | format: format
173 | };
174 | let dimensions;
175 | try {
176 | dimensions = _abaculus.coordsFromBbox(opts.zoom, opts.scale, opts.bbox, MAX_SIZE4)
177 | while (dimensions.w > MAX_SIZE || dimensions.h > MAX_SIZE) {
178 | opts.zoom--;
179 | dimensions = _abaculus.coordsFromBbox(opts.zoom, opts.scale, opts.bbox, MAX_SIZE4);
180 | }
181 | } catch (e) {
182 | return makeError(e.message, 'InvalidDimensionValue');
183 | }
184 |
185 | var image, resizedImage;
186 | try {
187 | image = await abaculus(opts);
188 | } catch (e) {
189 | return makeUnknownError(e);
190 | }
191 | if (abort.aborted) {
192 | return makeUnknownError(new Error('aborted'));
193 | }
194 | try {
195 | resizedImage = await resize(image, format, width, height, dimensions.w, dimensions.h);
196 | } catch (e) {
197 | return makeError(e && e.message || 'error resizing image, please check dimensions');
198 | }
199 | if (abort.aborted) {
200 | return makeUnknownError(new Error('aborted'));
201 | }
202 | var headers = {
203 | 'content-type': mime.lookup(format),
204 | 'content-length': resizedImage.length,
205 | etag: getEtag(resizedImage)
206 | };
207 | return {
208 | data: resizedImage,
209 | headers: headers,
210 | code: 200
211 | };
212 |
213 | }
214 |
215 | function getZoom(bounds, dimensions, minzoom, maxzoom) {
216 | minzoom = (minzoom === undefined) ? 0 : minzoom;
217 | maxzoom = (maxzoom === undefined) ? 20 : maxzoom;
218 |
219 |
220 | var bl = merc.px([bounds[0], bounds[1]], maxzoom);
221 | var tr = merc.px([bounds[2], bounds[3]], maxzoom);
222 | var width = tr[0] - bl[0];
223 | var height = bl[1] - tr[1];
224 | var ratios = [width / dimensions[0], height / dimensions[1]];
225 | var adjusted = Math.ceil(Math.min(
226 | maxzoom - (Math.log(ratios[0]) / Math.log(2)),
227 | maxzoom - (Math.log(ratios[1]) / Math.log(2))));
228 | return Math.max(minzoom, Math.min(maxzoom, adjusted));
229 |
230 | }
231 | module.exports = function(layer, params, cache, callback) {
232 | if (typeof cache === 'function') {
233 | callback = cache;
234 | cache = undefined;
235 | }
236 | if (typeof cache === 'undefined') {
237 | cache = defaultCache;
238 | }
239 | return Promise.resolve(getMap(layer, params, cache)).asCallback(callback);
240 | };
241 | var blankPNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABmJLR0QA/wD/AP+gvaeTAAABFUlEQVR42u3BMQEAAADCoPVP7WsIoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAMBPAAB2ClDBAAAAABJRU5ErkJggg==', 'base64');
242 | var blanks = new Map();
243 |
244 | function callBlankBack(tile, format, callback) {
245 | var headers = {
246 | 'content-type': mime.lookup(format),
247 | 'content-length': tile.length
248 | };
249 | callback(null, tile, headers);
250 | }
251 |
252 | function sendBlank(bgcolor, format, callback) {
253 | if (!format) {
254 | return callback(new Error('not found'));
255 | }
256 | if (bgcolor === false) {
257 | return callBlankBack(blankPNG, format, callback);
258 | }
259 | if (bgcolor.slice(0, 2) === '0x') {
260 | bgcolor = bgcolor.slice(2);
261 | }
262 | if (bgcolor.length === 3) {
263 | bgcolor = bgcolor[0] + bgcolor[0] + bgcolor[1] + bgcolor[1] + bgcolor[2] + bgcolor[2];
264 | }
265 | if (bgcolor.length !== 6) {
266 | bgcolor = 'ffffff';
267 | }
268 | var key = bgcolor + format;
269 | if (blanks.has(key)) {
270 | return callBlankBack(blanks.get(key), format, callback);
271 | }
272 | makeTile(bgcolor, format, function(err, image) {
273 | if (err) {
274 | return callback(err);
275 | }
276 | if (!blanks.has(key)) {
277 | blanks.set(key, image);
278 | }
279 | callBlankBack(image, format, callback);
280 | });
281 | }
282 |
283 | function makeTile(bgcolor, format, cb) {
284 | var image = new Image(256, 256);
285 | image.fill(new Color('#' + bgcolor + 'ff'), function(err, resp) {
286 | if (err) {
287 | return cb(err);
288 | }
289 | resp.encode(format, cb);
290 | });
291 | }
292 |
293 | function makeGetTile(layer, bgcolor, format) {
294 | return getTile;
295 |
296 | function getTile(z, x, y, callback) {
297 | if (checkTile(layer, x, y, z)) {
298 | return sendBlank(bgcolor, format, callback);
299 | }
300 | var aborted = false;
301 | var req;
302 | actualGet(true);
303 | return {
304 | abort() {
305 | aborted = true;
306 | if (req) {
307 | req.abort();
308 | }
309 | }
310 | }
311 |
312 | function actualGet(retry) {
313 | req = layer.getTile(z, x, y, function(err, tile, headers) {
314 | req = null;
315 | if (err) {
316 | if (retry && err.message === 'timeout' && !aborted) {
317 | return actualGet();
318 | }
319 | return sendBlank(bgcolor, format, callback);
320 | }
321 | callback(null, tile, headers);
322 | });
323 | }
324 | }
325 | }
326 |
327 | function makeLayer(layerArray, layerParam, bgcolor, format, abort) {
328 | if (layerParam.indexOf(',') === -1) {
329 | return find(layerArray, layerParam);
330 | }
331 | var layers = layerParam.split(',');
332 | if (typeof layerArray.layerLimit === 'number' && layers.length > layerArray.layerLimit) {
333 | var e = new Error(`LayerLimit is ${layerArray.layerLimit} but ${layers.length} layers were requested`);
334 | e.layerLengthError = true;
335 | throw e;
336 | }
337 | var layerObjects = layers.map(function(item) {
338 | return find(layerArray, item);
339 | });
340 | if (layerObjects.some(function(layer) {
341 | return !layer || layer.viewable === false;
342 | })) {
343 | return false;
344 | }
345 | return layerObjects.reduce(function(acc, item) {
346 | acc.range = [Math.min(acc.range[0], item.range[0]), Math.max(acc.range[1], item.range[1])];
347 | acc.bbox = [Math.min(acc.bbox[0], item.bbox[0]), Math.min(acc.bbox[1], item.bbox[1]),
348 | Math.max(acc.bbox[2], item.bbox[2]), Math.max(acc.bbox[3], item.bbox[3])
349 | ];
350 | return acc;
351 | }, {
352 | range: [Infinity, -Infinity],
353 | bbox: [Infinity, Infinity, -Infinity, -Infinity],
354 | getTile: makeGetTiles(layerObjects, bgcolor, format, abort)
355 | });
356 | }
357 | var getFuncMap = new WeakMap();
358 |
359 | function noop() {}
360 | async function parallel (list, fun) {
361 | var i = list.length;
362 | var out = [];
363 | while (i--) {
364 | let result = await fun(list[i]);
365 | if (!result) {
366 | continue;
367 | }
368 | out.push(result);
369 | if (imageType(result.tile) === 'image/jpeg') {
370 | break;
371 | }
372 | }
373 | out.reverse();
374 | return out;
375 | }
376 |
377 | function makeGetTiles(layerObjects, bgcolor, format, abort) {
378 | var aborts = new Set();
379 |
380 | function cancelAll() {
381 | aborts.forEach(function(f) {
382 | f();
383 | });
384 | }
385 | abort.on('abort', cancelAll);
386 | return getTile;
387 |
388 | function getTile(z, x, y, callback) {
389 | parallel(layerObjects, function(layer) {
390 | return new Promise(function(resolve) {
391 | var getTile;
392 | if (getFuncMap.has(layer)) {
393 | getTile = getFuncMap.get(layer);
394 | } else {
395 | getTile = makeGetTile(layer);
396 | getFuncMap.set(layer, getTile);
397 | }
398 | var cancel;
399 | var out = getTile(z, x, y, function(err, tile, headers) {
400 | aborts.delete(cancel);
401 | if (err) {
402 | return resolve();
403 | }
404 | resolve({
405 | tile: tile,
406 | headers: headers
407 | });
408 | });
409 | cancel = out.abort || noop;
410 | aborts.add(cancel);
411 | });
412 | }).then(function(things) {
413 | abort.removeListener('abort', cancelAll);
414 | var filtered = things.filter(function(item) {
415 | return item;
416 | });
417 | if (!filtered.length) {
418 | return sendBlank(bgcolor, format, callback);
419 | }
420 | if (filtered.length === 1) {
421 | return callback(null, filtered[0].tile, filtered[0].headers);
422 | }
423 | blend(filtered.map(function(item) {
424 | return item.tile;
425 | }), {
426 | format: 'png',
427 | width: 256,
428 | height: 256
429 | }, function(err, resp) {
430 | if (err || abort.aborted) {
431 | return sendBlank(bgcolor, format, callback);
432 | }
433 |
434 | var headers = {
435 | etag: getEtag(resp),
436 | 'Content-Type': 'image/png'
437 | };
438 | callback(null, resp, headers);
439 | });
440 | });
441 | }
442 | }
443 |
--------------------------------------------------------------------------------
/lib/memCache.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Cache = require('lru-cache');
3 |
4 | var lru = new Cache({
5 | max: 20 * 1024 * 1024,
6 | length: function (item) {
7 | return item.length;
8 | }
9 | });
10 |
11 | exports.get = get;
12 | function get (key, callback) {
13 | var item = lru.get(key);
14 | if (Buffer.isBuffer(item)) {
15 | return process.nextTick(callback, null, item);
16 | }
17 | process.nextTick(callback, new Error('not found'));
18 | }
19 |
20 | exports.set = set;
21 |
22 | function set(key, value, callback) {
23 | if (!Buffer.isBuffer(value)) {
24 | value = Buffer.from(value);
25 | }
26 | lru.set(key, value);
27 | process.nextTick(callback);
28 | }
29 |
--------------------------------------------------------------------------------
/lib/merc.js:
--------------------------------------------------------------------------------
1 | module.exports = new (require('@mapbox/sphericalmercator'))();
2 |
--------------------------------------------------------------------------------
/lib/normalizeObj.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = normalizeObj;
4 | function normalizeObj(obj) {
5 | return Object.keys(obj).reduce(function (acc, item) {
6 | acc[item.toLowerCase()] = obj[item];
7 | return acc;
8 | }, {});
9 | }
10 |
--------------------------------------------------------------------------------
/lib/promise.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Promise = require('bluebird');
3 |
4 | Promise.coroutine.addYieldHandler(function(v) {
5 | return Promise.resolve(v);
6 | });
7 |
8 | module.exports = Promise;
9 |
--------------------------------------------------------------------------------
/lib/tile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Promise = require('./promise');
3 | var checkTile = require('./checkTile');
4 | var getEtag = require('./getEtag');
5 | var makeError = require('./makeError');
6 | var mime = require('mime');
7 | var mapnik = require('mapnik');
8 | var normalizeObj = require('./normalizeObj');
9 | var find = require('./find');
10 | var makeUnknownError = require('./makeUnknownError');
11 | var defaultCache = Promise.promisifyAll(require('./memCache'));
12 | var crypto = require('crypto');
13 | var EE = require('events').EventEmitter;
14 | var imageType = require('./image-type');
15 | module.exports = function(layer, params, cache, callback) {
16 | if (typeof cache === 'function') {
17 | callback = cache;
18 | cache = undefined;
19 | }
20 | if (typeof cache === 'undefined') {
21 | cache = defaultCache;
22 | } else {
23 | cache = Promise.promisifyAll(cache);
24 | }
25 | return Promise.resolve(getTile(layer, params, cache)).asCallback(callback);
26 | };
27 | var requiredParams = [
28 | 'layer',
29 | 'tilematrix',
30 | 'tilerow',
31 | 'tilecol',
32 | 'format'
33 | ];
34 |
35 | async function getTile(layer, rawParams, cache) {
36 | try {
37 | layer = await layer;
38 | } catch (e) {
39 | return makeUnknownError(e);
40 | }
41 | if (!layer) {
42 | throw new TypeError('must include layer function');
43 | }
44 | var params = normalizeObj(rawParams);
45 | var i = -1;
46 | var len = requiredParams.length;
47 | var param;
48 | while (++i < len) {
49 | param = requiredParams[i];
50 | if (!params[param]) {
51 | return makeError(`missing required parameter: ${param}`, 'MissingParameterValue', param);
52 | }
53 | }
54 | var abort = layer.abort;
55 | layer = find(layer, params.layer);
56 | if (!layer) {
57 | return makeError(`No such layer: ${params.layer}`, 'InvalidParameterValue', 'LAYER');
58 | }
59 | if (layer.viewable === false) {
60 | return {
61 | headers: {},
62 | data: Buffer.from('not authorized'),
63 | code: 401
64 | };
65 | }
66 | layer.abort = abort || new EE();
67 | var desiredFormat = mime.extension(params.format);
68 | if (!desiredFormat) {
69 | return makeError(`Invalid paramter value for FORMAT: ${params.format}`, 'InvalidParameterValue', 'FORMAT');
70 | }
71 | var z;
72 | if (params.tilematrix.indexOf(':') > -1) {
73 | z = parseInt(params.tilematrix.split(':')[1], 10);
74 | } else {
75 | z = parseInt(params.tilematrix, 10);
76 | }
77 | var y = parseInt(params.tilerow, 10);
78 | var x = parseInt(params.tilecol, 10);
79 | var paramError = checkXYZ(z, params.tilematrix, 'TILEMATRIX') ||
80 | checkXYZ(y, params.tilerow, 'TILEROW') ||
81 | checkXYZ(x, params.tilecol, 'TILECOL');
82 | if (paramError) {
83 | return paramError;
84 | }
85 | var tileError = checkTile(layer, x, y, z);
86 | if (tileError) {
87 | return makeError(`${tileError} is out of range`, 'TileOutOfRange', tileError);
88 | }
89 | var getTileFun = (z, x, y) => new Promise((success, failure) => {
90 | var ret = layer.getTile(z, x, y, function(err, tile, headers) {
91 | layer.abort.removeListener('abort', abortReq);
92 | if (err) {
93 | return failure(err);
94 | }
95 | return success([tile, headers]);
96 | });
97 | layer.abort.on('abort', abortReq);
98 |
99 | function abortReq() {
100 | ret.abort();
101 | failure(new Error('aborted'));
102 | }
103 | });
104 | var tile;
105 | try {
106 | tile = await getTileFun(z, x, y);
107 | } catch (e) {
108 | var err = makeUnknownError('tile not found');
109 | err.code = 404;
110 | return err;
111 | }
112 | try {
113 | var data = tile[0];
114 | if (typeof data === 'string') {
115 | data = Buffer.from(data, 'utf8');
116 | }
117 |
118 | var headers = normalizeObj(tile[1]);
119 | headers['content-type'] = imageType(data);
120 | var returnedFormat = mime.extension(headers['content-type']);
121 | if (returnedFormat === desiredFormat) {
122 | if (!headers['content-length']) {
123 | headers['content-length'] = data.length;
124 | }
125 | if (!headers.etag) {
126 | headers.etag = getEtag(data);
127 | }
128 | return {
129 | data: data,
130 | headers: headers,
131 | code: 200
132 | };
133 | }
134 | var transcodedData = await transCode(data, desiredFormat, cache, layer.abort);
135 | headers['content-type'] = mime.lookup(desiredFormat);
136 | headers['content-length'] = transcodedData.length;
137 | headers.etag = getEtag(data);
138 | return {
139 | data: transcodedData,
140 | headers: headers,
141 | code: 200
142 | };
143 | } catch (e) {
144 | return makeUnknownError(e);
145 | }
146 | }
147 | var fromBytes = Promise.promisify(mapnik.Image.fromBytes, mapnik.Image);
148 |
149 | function encode(image, format) {
150 | if (!image || typeof image.encode !== 'function') {
151 | return Promise.reject(new TypeError('invalid image object'));
152 | }
153 | return new Promise(function(resolve, reject) {
154 | image.encode(format, function(err, resp) {
155 | if (err) {
156 | return reject(err);
157 | }
158 | return resolve(resp);
159 | });
160 | });
161 | }
162 |
163 | async function transCode(tile, desiredFormat, cache, abort) {
164 | var key = crypto.createHash('sha224').update(tile).update(desiredFormat).digest('base64');
165 | try {
166 | return await cache.getAsync(key);
167 | } catch (_) {
168 | // pass
169 | }
170 | if (abort.aborted) {
171 | throw new Error('aborted');
172 | }
173 | var image = await fromBytes(tile);
174 | if (abort.aborted) {
175 | throw new Error('aborted');
176 | }
177 | var encoded = await encode(image, desiredFormat);
178 | if (abort.aborted) {
179 | throw new Error('aborted');
180 | }
181 | await cache.setAsync(key, encoded);
182 | return encoded;
183 | }
184 |
185 | function checkXYZ(value, orig, name) {
186 | if (value !== value) {
187 | return makeError(`Invalid paramter value for ${name}: ${orig}`, 'InvalidParameterValue', name);
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | Copyright (C) 2015 Applied Geographics, Inc. & Calvin W. Metcalf
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wms",
3 | "version": "4.2.1",
4 | "description": "WMS/WMTS services for node.js",
5 | "main": "lib",
6 | "author": "Calvin W. Metcalf ",
7 | "license": "MIT",
8 | "scripts": {
9 | "test": "tape test/*.js"
10 | },
11 | "dependencies": {
12 | "@mapbox/sphericalmercator": "^1.0.5",
13 | "bluebird": "^2.9.34",
14 | "d3-queue": "^3.0.7",
15 | "handlebars": "^4.0.0",
16 | "lru-cache": "^2.6.5",
17 | "mapnik": "^4.2.1",
18 | "mime": "^1.3.4"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git@github.com:AppGeo/wms.git"
23 | },
24 | "devDependencies": {
25 | "eslint": "^4.1.1",
26 | "tape": "^4.1.0",
27 | "babel-eslint": "^7.2.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | wms
2 | ====
3 |
4 | [](https://travis-ci.org/AppGeo/wms)
5 |
6 | WMS and WMTS web service for Node.js.
7 |
8 | Only works in spherical web mercator, expects a tiled source, requires Graphics Magic to be available, and a version of node that has support for ES6 features (specifically template literals, generators, and WeakMaps).
9 |
10 | API
11 | ---
12 |
13 | The module is a function which takes 2 required parameters and 2 optional ones. The first is service config, the full config is below, but is only mandatory for `getCapabilities`, `getMap` and `getTile` may pass just the layers array or just the layer object (for `getTile` and `getMap` with only one layer requested).
14 |
15 | - **title**: title for the service
16 | - **abstract**: short description
17 | - **host**: the host where the service lives
18 | - **wmshost**: if you want the wms to be on a different host populate this, otherwise will default to host
19 | - **layerLimit**: if you want to specify a limit on the number layers in wms requests, do so here
20 | - **layers**: array of layer objects with the following keys
21 | - **viewable**: if set to false then the layer won't be included
22 | - **title**: title of the layer
23 | - **name**: identifier for the layer
24 | - **bbox**: bounding box for the layer in, array in [minx, miny, minx, miny] format
25 | - **range**: array containing the min and max zooms in terms of tile zooms in [minZoom, maxZoom] format.
26 | - **image**: Array with the image types the layer should support, defaults to `['png', 'jpeg']` which are also the only two options, mainly relevant because arcgis offers no ability to select image format (defaulting to 'jpeg' making it impossible to pick png if jpeg is available).
27 | - **getTile**: function called to get tiles, not needed for `getCapabilities`.
28 |
29 | The second argument is the query string. These are the arguments being sent to the server via query string e.g. in Express it's `req.query`.
30 |
31 | A cache object may also be supplied as the 3rd argument (detailed bellow).
32 |
33 | Either a callback can be supplied or if not it returns a promise (this is the half a parameter).
34 |
35 | The callback is called with (or the promise resolves with) an object with the following properties:
36 |
37 | - **data**: a buffer with the data to return
38 | - **headers**: headers
39 | - **code**: status code
40 |
41 | So with Express you can just call
42 |
43 | ```js
44 | var wms = require('wms');
45 | app.get('/something', function (req, res){
46 | wms(layerInfo, req.query).then(function (wmsResponse) {
47 | res.set(wmsResponse.headers);
48 | res.status(wmsResponse.code);
49 | res.send(wmsResponse.data);
50 | });
51 | });
52 | ```
53 |
54 | getTile function
55 | ----------------
56 |
57 | Both WMS and WMTS require a `getTile` function which is called with `zoom`, `level`, and `row` of a tile and a callback. The callback needs to be called with the time or an error or the tile and buffer. For example:
58 |
59 | ```js
60 | function(z, x, y, callback){
61 | // do something
62 | return callback(null, buffer, headers);
63 | }
64 | ```
65 |
66 | This will not be called if the tile is outside of the bounding box or zoom range.
67 |
68 |
69 | Cache Object
70 | ===
71 |
72 | The cache object which may be supplied as the 3rd argument is an object with get and set, get takes a string and a callback, set takes a string, a buffer, and a callback.
73 |
74 | Get returns either the object from the key, or it calls the callback with an error.
75 |
76 | If this object is omitted then a simple lru memory cache is used, currently is only used to cache the image transforms in the WMTS service.
77 |
78 | Should you use a WMS or a WMTS?
79 | -------------------------------
80 |
81 | Always use a WMTS over a WMS if given a choice. Only ever use a WMS if you don't have a choice. WMS is very slow. This is not a bug; this is a consequence of how it works.
82 |
83 | Even better then a WMTS server - use a TMS as it allows the server to make and send tiles in arbitrary and mixed formats.
84 |
85 | Spec Compliance
86 | ---------------
87 |
88 | This implementation of a WMS/WMTS server is written with a sensibility more akin to JSON then XML. In other words, mandatory parameters that can be inferred from context do not cause an exception to be throw. For instance: since only WMTS supports `GetTile`, and only WMS supports `GetMap`, and the only request they both support is `GetCapabilities`, omitting the `service` parameter will only throw an error on a `GetCapabilities` request.
89 |
90 | ~~Despite the incredible detail paid to certain areas of the spec, there are situations that are are not covered. For instance, with a WMTS service, the only way to communicate that a tile was not found in the cache is to tell the client that it was out of range. In other words, it does not cover the case where the data behind the cache might not be a perfect square as might be the case for, say, state-level imagery outside of the great plains. In cases like this, I've tried to be the least surprising as I can, and when conventions collide, I err on the side of the more widely used convention. So in this case, I return a `NoApplicableCode` exception report with a 404 status code. This uses the convention of issuing 404 status codes to indicate missing resources, in favor of the OGC specific convention of responding to requests for non existent resources with 400.~~
91 |
92 | QGIS retries 404's because it is too dumb to live so I just am sending back blank tiles in the case of 404s.
93 |
94 | The OGC spec fundamentally believes that any request for data that doesn't exist is malformed by definition, because well formed requests would get data. A strict reading of the spec (specifically table 24 of the WMTS spec) would imply I should use a 500 status code for uses of `NoApplicableCode` because, from what I can tell, the OGC feels that for a server to get into the situation where there is no applicable OGC exception code would by necessity be a server error, because if the server was constructed properly it would never be in this situation.
95 |
96 | It is currently not possible to specify additional `SRS` codes for WMS requests, but should in theory be possible via GDAL. This faces the road block of GDAL hating you and not supporting stdin/stdout, and the fact it would cause the already slow WMS to be even slower.
97 |
98 | The chances of adding in additional tile pyramids are approximately 0, as the massive increase in complexity would likely not bring in much useful benefits as it is rare to see the same custom tile pyramid used by 2 different groups, and almost unheard of to see the same custom tile pyramid used across state lines.
99 |
100 | License
101 | -------
102 |
103 | [MIT](license.md)
104 |
--------------------------------------------------------------------------------
/templates/wms.hbs:
--------------------------------------------------------------------------------
1 |
2 |
5 | ]>
6 |
7 |
8 | OGC:WMS
9 | {{title}} - WMS
10 |
11 |
12 | none
13 | none
14 | {{#if layerLimit}}
15 | {{layerLimit}}
16 | {{/if}}
17 | 19008
18 | 19008
19 |
20 |
21 |
22 |
23 | application/vnd.ogc.wms_xml
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | image/png
32 | image/jpeg
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | application/vnd.ogc.se_xml
43 |
44 |
45 |
46 | {{title}}
47 | EPSG:3857
48 |
49 |
50 | {{#each layers}}
51 |
52 | {{name}}
53 | {{title}}
54 |
55 | {{! }}
56 |
57 |
58 |
59 | {{/each}}
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/templates/wmts.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}} - WMTS
5 |
6 | OGC WMTS
7 | 1.0.0
8 | none
9 | none
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | RESTful
19 |
20 |
21 |
22 |
23 |
24 |
25 | KVP
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | KVP
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{#each layers}}
48 |
49 | {{title}}
50 |
51 |
52 | {{mercminx}} {{mercminy}}
53 | {{mercmaxx}} {{mercmaxy}}
54 |
55 |
56 | {{minx}} {{miny}}
57 | {{maxx}} {{maxy}}
58 |
59 | {{name}}
60 |
63 | {{#if jpeg}}
64 | image/jpeg
65 | {{/if}}
66 | {{#if png}}
67 | image/png
68 | {{/if}}
69 |
70 | {{tileset}}
71 | {{#each tilelimits}}
72 |
73 | {{../tileset}}:{{zoom}}
74 | {{bbox.minX}}
75 | {{bbox.maxX}}
76 | {{bbox.minY}}
77 | {{bbox.maxY}}
78 |
79 | {{/each}}
80 |
81 | {{#if jpeg}}
82 |
85 | {{/if}}
86 | {{#if png}}
87 |
90 | {{/if}}
91 |
92 | {{/each}}
93 | {{#each tilematrix}}
94 |
95 | {{name}}
96 | EPSG:3857
97 | {{#if level0}}
98 |
99 | {{name}}:00
100 | 559082264.029
101 | -20037508.3428 20037508.3428
102 | 256
103 | 256
104 | 1
105 | 1
106 |
107 | {{/if}}
108 | {{#if level1}}
109 |
110 | {{name}}:01
111 | 279541132.014
112 | -20037508.3428 20037508.3428
113 | 256
114 | 256
115 | 2
116 | 2
117 |
118 | {{/if}}
119 | {{#if level2}}
120 |
121 | {{name}}:02
122 | 139770566.007
123 | -20037508.3428 20037508.3428
124 | 256
125 | 256
126 | 4
127 | 4
128 |
129 | {{/if}}
130 | {{#if level3}}
131 |
132 | {{name}}:03
133 | 69885283.0036
134 | -20037508.3428 20037508.3428
135 | 256
136 | 256
137 | 8
138 | 8
139 |
140 | {{/if}}
141 | {{#if level4}}
142 |
143 | {{name}}:04
144 | 34942641.5018
145 | -20037508.3428 20037508.3428
146 | 256
147 | 256
148 | 16
149 | 16
150 |
151 | {{/if}}
152 | {{#if level5}}
153 |
154 | {{name}}:05
155 | 17471320.7509
156 | -20037508.3428 20037508.3428
157 | 256
158 | 256
159 | 32
160 | 32
161 |
162 | {{/if}}
163 | {{#if level6}}
164 |
165 | {{name}}:06
166 | 8735660.37545
167 | -20037508.3428 20037508.3428
168 | 256
169 | 256
170 | 64
171 | 64
172 |
173 | {{/if}}
174 | {{#if level7}}
175 |
176 | {{name}}:07
177 | 4367830.18772
178 | -20037508.3428 20037508.3428
179 | 256
180 | 256
181 | 128
182 | 128
183 |
184 | {{/if}}
185 | {{#if level8}}
186 |
187 | {{name}}:08
188 | 2183915.09386
189 | -20037508.3428 20037508.3428
190 | 256
191 | 256
192 | 256
193 | 256
194 |
195 | {{/if}}
196 | {{#if level9}}
197 |
198 | {{name}}:09
199 | 1091957.54693
200 | -20037508.3428 20037508.3428
201 | 256
202 | 256
203 | 512
204 | 512
205 |
206 | {{/if}}
207 | {{#if level10}}
208 |
209 | {{name}}:10
210 | 545978.773466
211 | -20037508.3428 20037508.3428
212 | 256
213 | 256
214 | 1024
215 | 1024
216 |
217 | {{/if}}
218 | {{#if level11}}
219 |
220 | {{name}}:11
221 | 272989.386733
222 | -20037508.3428 20037508.3428
223 | 256
224 | 256
225 | 2048
226 | 2048
227 |
228 | {{/if}}
229 | {{#if level12}}
230 |
231 | {{name}}:12
232 | 136494.693366
233 | -20037508.3428 20037508.3428
234 | 256
235 | 256
236 | 4096
237 | 4096
238 |
239 | {{/if}}
240 | {{#if level13}}
241 |
242 | {{name}}:13
243 | 68247.3466832
244 | -20037508.3428 20037508.3428
245 | 256
246 | 256
247 | 8192
248 | 8192
249 |
250 | {{/if}}
251 | {{#if level14}}
252 |
253 | {{name}}:14
254 | 34123.6733416
255 | -20037508.3428 20037508.3428
256 | 256
257 | 256
258 | 16384
259 | 16384
260 |
261 | {{/if}}
262 | {{#if level15}}
263 |
264 | {{name}}:15
265 | 17061.8366708
266 | -20037508.3428 20037508.3428
267 | 256
268 | 256
269 | 32768
270 | 32768
271 |
272 | {{/if}}
273 | {{#if level16}}
274 |
275 | {{name}}:16
276 | 8530.9183354
277 | -20037508.3428 20037508.3428
278 | 256
279 | 256
280 | 65536
281 | 65536
282 |
283 | {{/if}}
284 | {{#if level17}}
285 |
286 | {{name}}:17
287 | 4265.4591677
288 | -20037508.3428 20037508.3428
289 | 256
290 | 256
291 | 131072
292 | 131072
293 |
294 | {{/if}}
295 | {{#if level18}}
296 |
297 | {{name}}:18
298 | 2132.72958385
299 | -20037508.3428 20037508.3428
300 | 256
301 | 256
302 | 262144
303 | 262144
304 |
305 | {{/if}}
306 | {{#if level19}}
307 |
308 | {{name}}:19
309 | 1066.36479192
310 | -20037508.3428 20037508.3428
311 | 256
312 | 256
313 | 524288
314 | 524288
315 |
316 | {{/if}}
317 | {{#if level20}}
318 |
319 | {{name}}:20
320 | 533.18239596
321 | -20037508.3428 20037508.3428
322 | 256
323 | 256
324 | 1048576
325 | 1048576
326 |
327 | {{/if}}
328 | {{#if level21}}
329 |
330 | {{name}}:21
331 | 266.59119798
332 | -20037508.3428 20037508.3428
333 | 256
334 | 256
335 | 2097152
336 | 2097152
337 |
338 | {{/if}}
339 |
340 | {{/each}}
341 |
342 |
343 |
--------------------------------------------------------------------------------
/test/basic.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var test = require('tape');
4 |
5 | var wms = require('../lib');
6 |
7 | var qs = require('querystring');
8 | // - **title**: title for the service
9 | // - **abstract**: short description
10 | // - **host**: the host where the service lives
11 | // - **layers**: array of layer objects with the following keys
12 | // - **viewable**: if set to false then the layer won't be included
13 | // - **title**: title of the layer
14 | // - **name**: identifier for the layer
15 | // - **bbox**: bounding box for the layer in, array in [minx, miny, minx, miny] format
16 | // - **range**: array containing the min and max zooms in terms of tile zooms in [minZoom, maxZoom] format.
17 | // - **image**: Array with the image types the layer should support, defaults to `['png', 'jpeg']` which are also the only two options, mainly relevant because arcgis offers no ability to select image format (defaulting to 'jpeg' making it impossible to pick png if jpeg is available).
18 | // - **getTile**:
19 | var fakeTile = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABmJLR0QA/wD/AP+gvaeTAAABFUlEQVR42u3BMQEAAADCoPVP7WsIoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAMBPAAB2ClDBAAAAABJRU5ErkJggg==', 'base64');
20 | var fakeHeaders = {
21 | 'content-type': 'image/png',
22 | foo: 'bar'
23 | };
24 | var obj = {
25 | title: 'a title',
26 | abstract: 'not like picaso',
27 | host: 'abc://blah.blah',
28 | layers: [
29 | {
30 | title: 'my layer',
31 | name: 'jeffery',
32 | bbox: [-106.787109375, 25.8394494020632,
33 | -93.427734375, 36.6331620955865],
34 | range: [0, 20],
35 | getTile: function (a, b, c, cb) {
36 | process.nextTick(cb, null, fakeTile, fakeHeaders);
37 | }
38 | }
39 | ]
40 | };
41 | test('basic wms', function (t) {
42 | t.plan(3);
43 | wms(obj, {
44 | service: 'wms',
45 | request: 'GetCapabilities'
46 | }).then(function (resp) {
47 | t.ok(Buffer.isBuffer(resp.data), 'got a buffer');
48 | t.equals(resp.code, 200, 'correct code');
49 | t.ok(resp.headers, 'got headers');
50 | }).catch(function (e) {
51 | t.notOk(e);
52 | });
53 | });
54 | test('basic wmts', function (t) {
55 | t.plan(3);
56 | wms(obj, {
57 | service: 'wmts',
58 | request: 'GetCapabilities'
59 | }).then(function (resp) {
60 | t.ok(Buffer.isBuffer(resp.data), 'got a buffer');
61 | t.equals(resp.code, 200, 'correct code');
62 | t.ok(resp.headers, 'got headers');
63 | }).catch(function (e) {
64 | t.notOk(e);
65 | });
66 | });
67 | test('basic wmts getTile request', function (t) {
68 | t.plan(4);
69 | wms(obj, qs.parse('service=WMTS&request=GetTile&version=1.0.0&layer=jeffery&style=default&format=image/png&TileMatrixSet=0to20&TileMatrix=0to20:18&TileRow=105976&TileCol=62375')
70 | ).then(function (resp) {
71 | t.ok(Buffer.isBuffer(resp.data), 'got a buffer');
72 | t.equals(resp.data.toString('hex'), fakeTile.toString('hex'), 'same data');
73 | t.equals(resp.code, 200, 'correct code');
74 | t.equals(resp.headers.foo, 'bar', 'got custom headers');
75 | }).catch(function (e) {
76 | t.notOk(e);
77 | });
78 | });
79 | test('basic wmts getTile for out of range request', function (t) {
80 | t.plan(4);
81 | wms(obj, qs.parse('service=WMTS&request=GetTile&version=1.0.0&layer=jeffery&style=default&format=image/png&TileMatrixSet=0to20&TileMatrix=0to20:18&TileRow=63040&TileCol=62375')
82 | ).then(function (resp) {
83 | t.ok(Buffer.isBuffer(resp.data), 'got a buffer');
84 | t.equals(resp.data.toString(), '\n \n TILEROW is out of range\n \n\n', 'exception text');
85 | t.equals(resp.code, 400, 'correct code');
86 | t.ok(resp.headers, 'got headers');
87 | }).catch(function (e) {
88 | t.notOk(e);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------