From 2b8d7f79eb4a3962d60f37f96a7d9ba02269f8fd Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 22 Jun 2017 16:47:57 -0400 Subject: [PATCH 1/2] Increase options for tile url templates. Before, we would translate `{x}` -> tile x, `{y}` -> tile y, `{z}` -> tile layer, and `{s}` -> one of the subdomains defined in the layer. This makes these case insensitive, supports an options `$` prefix, and allows several additional subdomain specifications. Specifically, `{s:abc}`, `{S:abc}`, `{a-c}`, and `{a,b,c}` are all equivalent. If the subdomain is in the template string, the layer's subdomain list is ignored. When we refactor the documentation for the tileLayer, this information should be included in the subdomain and template string docs. --- src/tileLayer.js | 47 ++++++++++++++++++++++++++++++---------- tests/cases/tileLayer.js | 40 ++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/tileLayer.js b/src/tileLayer.js index a22f66dfa0..b563e61f1c 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -15,28 +15,53 @@ module.exports = (function () { /** * Pick a subdomain from a list of subdomains based on a the tile location. * - * @param {number} x: the x tile coordinate. - * @param {number} y: the y tile coordinate. - * @param {list} subdomains: the list of known subdomains. + * @param {number} x The x tile coordinate. + * @param {number} y The y tile coordinate. + * @param {number} z The tile layer. + * @param {string[]} subdomains The list of known subdomains. + * @returns {string} A subdomain based on the location. */ - function m_getTileSubdomain(x, y, subdomains) { - return subdomains[modulo(x + y, subdomains.length)]; + function m_getTileSubdomain(x, y, z, subdomains) { + return subdomains[modulo(x + y + z, subdomains.length)]; } /** * Returns an OSM tile server formatting function from a standard format - * string. Replaces {s}, {z}, {x}, and {y}. + * string. Replaces `{s}`, `{z}`, `{x}`, and `{y}`. These may be any case + * and may be prefixed with `$` (e.g., `${X}` is the same as `{x}`). The + * subdomain can be specifed by a string of characters, listed as a range, + * or as a comma separated list (e.g., `{s:abc}`, `{a-c}`, `{a,b,c}` are + * all equivalent. * * @param {string} base The tile format string - * @returns: a conversion function. + * @returns {function} A conversion function. * @private. */ function m_tileUrlFromTemplate(base) { + var xPattern = new RegExp(/\$?\{[xX]\}/), + yPattern = new RegExp(/\$?\{[yY]\}/), + zPattern = new RegExp(/\$?\{[zZ]\}/), + sPattern = new RegExp(/\$?\{(s|S|[sS]:.+|[^-{}]-[^-{}]|([^,{}]+,)+[^,{}]+)\}/); return function (x, y, z, subdomains) { - return base.replace('{s}', m_getTileSubdomain(x, y, subdomains)) - .replace('{z}', z) - .replace('{x}', x) - .replace('{y}', y); + var url = base + .replace(xPattern, x) + .replace(yPattern, y) + .replace(zPattern, z); + var sMatch = url.match(sPattern); + if (sMatch) { + if (sMatch[2]) { + subdomains = sMatch[1].split(','); + } else if (sMatch[1][1] === ':') { + subdomains = sMatch[1].substr(2).split(''); + } else if (sMatch[1][1] === '-') { + subdomains = []; + for (var i = sMatch[1].charCodeAt(0); i <= sMatch[1].charCodeAt(2); i += 1) { + subdomains.push(String.fromCharCode(i)); + } + } + url = url.replace(sPattern, m_getTileSubdomain(x, y, z, subdomains)); + } + return url; }; } diff --git a/tests/cases/tileLayer.js b/tests/cases/tileLayer.js index 9b2b9fc357..5a397453f6 100644 --- a/tests/cases/tileLayer.js +++ b/tests/cases/tileLayer.js @@ -648,8 +648,10 @@ describe('geo.tileLayer', function () { l._getTiles = function (level, bounds) { expect(level).toBe(1); expect(bounds).toEqual({ - left: 0, right: 1, - bottom: 0, top: 1 + left: 0, + right: 1, + bottom: 0, + top: 1 }); return [ {fetch: function () { @@ -1325,21 +1327,35 @@ describe('geo.tileLayer', function () { }); }); it('url templating', function () { + /* eslint-disable no-template-curly-in-string */ + var urls = { + 's={s}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], + 's=${S}&x=${X}&y=${Y}&z=${Z}': ['a', 'b', 'c'], + 's={s:abc}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], + 's={a,b,c}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], + 's={a-c}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], + 's=${s:1234}&x={x}&y={y}&z={z}': ['1', '2', '3', '4'], + 's=${1,2,3,4}&x={x}&y={y}&z={z}': ['1', '2', '3', '4'], + 's=${1-4}&x={x}&y={y}&z={z}': ['1', '2', '3', '4'], + 's={ab,bc,12}&x={x}&y={y}&z={z}': ['ab', 'bc', '12'] + }; + /* eslint-enable no-template-curly-in-string */ var tiles, l = geo.tileLayer({ map: map({unitsPerPixel: 1}), wrapX: false, wrapY: false, - topDown: true, - url: '/testdata/white.jpg?s={s}&x={x}&y={y}&z={z}' + topDown: true }); - - tiles = l._getTiles(1, {left: 50, right: 500, bottom: 500, top: 50}); - expect(tiles.length).toBe(5); - tiles.forEach(function (tile) { - expect($.inArray(tile._url.split('?s=')[1].split('&')[0], - ['a', 'b', 'c'])).toBeGreaterThan(-1); - expect(tile._url.split('&x')[1]).toBe('=' + tile.index.x + '&y=' + - tile.index.y + '&z=' + tile.index.level); + $.each(urls, function (url, subdomains) { + l.url('/testdata/white.jpg?' + url); + tiles = l._getTiles(1, {left: 50, right: 500, bottom: 500, top: 50}); + expect(tiles.length).toBe(5); + tiles.forEach(function (tile) { + expect($.inArray(tile._url.split('?s=')[1].split('&')[0], + subdomains)).toBeGreaterThan(-1); + expect(tile._url.split('&x')[1]).toBe('=' + tile.index.x + '&y=' + + tile.index.y + '&z=' + tile.index.level); + }); }); }); it('baseUrl', function () { From 212b1f824194efb460bafb18d0e30929f19be33a Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 23 Jun 2017 09:02:45 -0400 Subject: [PATCH 2/2] Refactor so that regexps aren't parsed on every tile. This parses the tile url template once instead of on every tile, then just does a string substitution per tile. It also handles character rangers specified in reverse (e.g., `{c-a}` is the same as `{a-c}`). --- src/tileLayer.js | 51 +++++++++++++++++++++++----------------- tests/cases/tileLayer.js | 1 + 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/tileLayer.js b/src/tileLayer.js index b563e61f1c..21d1ff416a 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -30,8 +30,9 @@ module.exports = (function () { * string. Replaces `{s}`, `{z}`, `{x}`, and `{y}`. These may be any case * and may be prefixed with `$` (e.g., `${X}` is the same as `{x}`). The * subdomain can be specifed by a string of characters, listed as a range, - * or as a comma separated list (e.g., `{s:abc}`, `{a-c}`, `{a,b,c}` are - * all equivalent. + * or as a comma-separated list (e.g., `{s:abc}`, `{a-c}`, `{a,b,c}` are + * all equivalent. The comma-separated list can have subdimains that are of + * any length; the string and range both use one-character subdomains. * * @param {string} base The tile format string * @returns {function} A conversion function. @@ -41,27 +42,35 @@ module.exports = (function () { var xPattern = new RegExp(/\$?\{[xX]\}/), yPattern = new RegExp(/\$?\{[yY]\}/), zPattern = new RegExp(/\$?\{[zZ]\}/), - sPattern = new RegExp(/\$?\{(s|S|[sS]:.+|[^-{}]-[^-{}]|([^,{}]+,)+[^,{}]+)\}/); - return function (x, y, z, subdomains) { - var url = base - .replace(xPattern, x) - .replace(yPattern, y) - .replace(zPattern, z); - var sMatch = url.match(sPattern); - if (sMatch) { - if (sMatch[2]) { - subdomains = sMatch[1].split(','); - } else if (sMatch[1][1] === ':') { - subdomains = sMatch[1].substr(2).split(''); - } else if (sMatch[1][1] === '-') { - subdomains = []; - for (var i = sMatch[1].charCodeAt(0); i <= sMatch[1].charCodeAt(2); i += 1) { - subdomains.push(String.fromCharCode(i)); - } + sPattern = new RegExp(/\$?\{(s|S|[sS]:[^{}]+|[^-{}]-[^-{}]|([^,{}]+,)+[^,{}]+)\}/); + var url = base + .replace(sPattern, '{s}') + .replace(xPattern, '{x}') + .replace(yPattern, '{y}') + .replace(zPattern, '{z}'); + var urlSubdomains; + var sMatch = base.match(sPattern); + if (sMatch) { + if (sMatch[2]) { + urlSubdomains = sMatch[1].split(','); + } else if (sMatch[1][1] === ':') { + urlSubdomains = sMatch[1].substr(2).split(''); + } else if (sMatch[1][1] === '-') { + urlSubdomains = []; + var start = sMatch[1].charCodeAt(0), + end = sMatch[1].charCodeAt(2); + for (var i = Math.min(start, end); i <= Math.max(start, end); i += 1) { + urlSubdomains.push(String.fromCharCode(i)); } - url = url.replace(sPattern, m_getTileSubdomain(x, y, z, subdomains)); } - return url; + } + + return function (x, y, z, subdomains) { + return url + .replace('{s}', m_getTileSubdomain(x, y, z, urlSubdomains || subdomains)) + .replace('{x}', x) + .replace('{y}', y) + .replace('{z}', z); }; } diff --git a/tests/cases/tileLayer.js b/tests/cases/tileLayer.js index 5a397453f6..7233b5057e 100644 --- a/tests/cases/tileLayer.js +++ b/tests/cases/tileLayer.js @@ -1334,6 +1334,7 @@ describe('geo.tileLayer', function () { 's={s:abc}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], 's={a,b,c}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], 's={a-c}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], + 's={c-a}&x={x}&y={y}&z={z}': ['a', 'b', 'c'], 's=${s:1234}&x={x}&y={y}&z={z}': ['1', '2', '3', '4'], 's=${1,2,3,4}&x={x}&y={y}&z={z}': ['1', '2', '3', '4'], 's=${1-4}&x={x}&y={y}&z={z}': ['1', '2', '3', '4'],