Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ In your client of choice, you can make either HTTP GET or POST requests.
`bounds` if provided must be `west,south,east,north` with floating point values (NO spaces or brackets)
`padding` if provided must be an integer value that is less than 1/2 of width or height, whichever is smaller. Can only be used with bounds.
`bearing is a floating point value (0-360)`pitch`is a floating point value (0-60)`token` if provided must a string
`images` is a JSON with image names as keys and image urls as values.

Images parameter is used when your style includes and `icon-image` property (e.g. festival.png). Server
will download the image from the url specified in this parameter and add it to the map
(e.g. {"festival.png": "http://images.com/festival.png"}). Only png images are supported for now.

Your style JSON needs to be URL encoded:

Expand Down
305 changes: 222 additions & 83 deletions dist/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ exports["default"] = exports.render = exports.normalizeMapboxGlyphURL = exports.

var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));

var _fs = _interopRequireDefault(require("fs"));
Expand All @@ -27,8 +29,12 @@ var _mbtiles = _interopRequireDefault(require("@mapbox/mbtiles"));

var _request = _interopRequireDefault(require("request"));

var _requestPromise = _interopRequireDefault(require("request-promise"));

var _url = _interopRequireDefault(require("url"));

var _pngjs = require("pngjs");

/* eslint-disable no-new */
// sharp must be before zlib and other imports or sharp gets wrong version of zlib and breaks on some servers
var TILE_REGEXP = RegExp('mbtiles://([^/]+)/(\\d+)/(\\d+)/(\\d+)');
Expand Down Expand Up @@ -290,44 +296,94 @@ var getLocalTile = function getLocalTile(tilePath, url, callback) {

var getRemoteTile = function getRemoteTile(url, callback) {
(0, _request["default"])({
url: url,
url: "".concat(url, "&force_retry"),
encoding: null,
gzip: true
}, function (err, res, data) {
if (err) {
return callback(err);
}

switch (res.statusCode) {
case 200:
{
return callback(null, {
data: data
});
}

case 204:
{
// No data for this url
return callback(null, {});
}

case 404:
{
// Tile not found
// this may be valid for some tilesets that have partial coverage
// on servers that do not return blank tiles in these areas.
console.warn("Missing tile at: ".concat(url));
return callback(null, {});
}

default:
{
// assume error
console.error("Error with request for: ".concat(url, "\nstatus: ").concat(res.statusCode));
return callback(new Error("Error with request for: ".concat(url, "\nstatus: ").concat(res.statusCode)));
}, function _callee(err, res, data) {
var retryAfter, retryAfterMs, retriedData;
return _regenerator["default"].async(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (!err) {
_context.next = 2;
break;
}

return _context.abrupt("return", callback(err));

case 2:
console.warn("res.statusCode: ".concat(res.statusCode));
_context.t0 = res.statusCode;
_context.next = _context.t0 === 200 ? 6 : _context.t0 === 202 ? 7 : _context.t0 === 204 ? 27 : _context.t0 === 404 ? 28 : 30;
break;

case 6:
return _context.abrupt("return", callback(null, {
data: data
}));

case 7:
if (res.headers['retry-after']) {
_context.next = 10;
break;
}

console.error("Error with request for: ".concat(url, "\nRequest for remote tile was accepted but no Retry-After header was sent."));
return _context.abrupt("return", callback(new Error("Error with request for: ".concat(url, "\nRequest for remote tile was accepted but no Retry-After header was sent."))));

case 10:
retryAfter = res.headers['retry-after'];
console.log("Retry-After header received. Attempting to retrieve tile in ".concat(retryAfter));
retryAfterMs = parseInt(retryAfter) * 1000;
_context.next = 15;
return _regenerator["default"].awrap(new Promise(function (resolve) {
return setTimeout(resolve, retryAfterMs);
}));

case 15:
_context.prev = 15;
_context.next = 18;
return _regenerator["default"].awrap((0, _requestPromise["default"])({
url: url,
encoding: null,
gzip: true
}));

case 18:
retriedData = _context.sent;
console.log('retriedData', retriedData);
return _context.abrupt("return", callback(null, {
data: retriedData
}));

case 23:
_context.prev = 23;
_context.t1 = _context["catch"](15);
console.error("Error with request for: ".concat(url, "\nstatus: ").concat(res.statusCode));
return _context.abrupt("return", callback(new Error("Error with request for: ".concat(url, "\nstatus: ").concat(res.statusCode))));

case 27:
return _context.abrupt("return", callback(null, {}));

case 28:
// Tile not found
// this may be valid for some tilesets that have partial coverage
// on servers that do not return blank tiles in these areas.
console.warn("Missing tile at: ".concat(url));
return _context.abrupt("return", callback(null, {}));

case 30:
// assume error
console.error("Error with request for: ".concat(url, "\nstatus: ").concat(res.statusCode));
return _context.abrupt("return", callback(new Error("Error with request for: ".concat(url, "\nstatus: ").concat(res.statusCode))));

case 32:
case "end":
return _context.stop();
}
}
}
}, null, null, [[15, 23]]);
});
};
/**
Expand Down Expand Up @@ -367,6 +423,85 @@ var getRemoteAsset = function getRemoteAsset(url, callback) {
}
});
};
/**
* Fetch a remotely hosted PNG image.
*
*
* @param {String} url - URL of the png image
*/


var loadPNG = function loadPNG(url) {
return (0, _requestPromise["default"])({
url: url,
encoding: null,
gzip: true
});
};
/**
* Adds a list of images to the map.
*
* @param {String} images - an object, image name to image url
* @param {Object} map - Mapbox GL Map
* @param {Function} callback - function to call after all images download, if any
*/


var loadImages = function loadImages(images, map, callback) {
var imageName, result, pngImage, imageOptions;
return _regenerator["default"].async(function loadImages$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
if (!(images !== null)) {
_context2.next = 18;
break;
}

_context2.t0 = _regenerator["default"].keys(images);

case 2:
if ((_context2.t1 = _context2.t0()).done) {
_context2.next = 18;
break;
}

imageName = _context2.t1.value;
_context2.prev = 4;
_context2.next = 7;
return _regenerator["default"].awrap(loadPNG(images[imageName]));

case 7:
result = _context2.sent;
pngImage = _pngjs.PNG.sync.read(result);
imageOptions = {
width: pngImage.width,
height: pngImage.height,
pixelRatio: 1
};
map.addImage(imageName, pngImage.data, imageOptions);
_context2.next = 16;
break;

case 13:
_context2.prev = 13;
_context2.t2 = _context2["catch"](4);
console.error("Error downloading image: ".concat(images[imageName]));

case 16:
_context2.next = 2;
break;

case 18:
callback();

case 19:
case "end":
return _context2.stop();
}
}
}, null, null, [[4, 13]]);
};
/**
* Render a map using Mapbox GL, based on layers specified in style.
* Returns a Promise with the PNG image data as its first parameter for the map image.
Expand Down Expand Up @@ -399,7 +534,9 @@ var render = function render(style) {
_options$ratio = options.ratio,
ratio = _options$ratio === void 0 ? 1 : _options$ratio,
_options$padding = options.padding,
padding = _options$padding === void 0 ? 0 : _options$padding;
padding = _options$padding === void 0 ? 0 : _options$padding,
_options$images = options.images,
images = _options$images === void 0 ? null : _options$images;
var _options$center = options.center,
center = _options$center === void 0 ? null : _options$center,
_options$zoom = options.zoom,
Expand Down Expand Up @@ -587,56 +724,58 @@ var render = function render(style) {
};
var map = new _mapboxGlNative["default"].Map(mapOptions);
map.load(style);
map.render({
zoom: zoom,
center: center,
height: height,
width: width,
bearing: bearing,
pitch: pitch
}, function (err, buffer) {
if (err) {
console.error('Error rendering map');
console.error(err);
return reject(err);
}

map.release(); // release map resources to prevent reusing in future render requests
// Un-premultiply pixel values
// Mapbox GL buffer contains premultiplied values, which are not handled correctly by sharp
// https://github.com/mapbox/mapbox-gl-native/issues/9124
// since we are dealing with 8-bit RGBA values, normalize alpha onto 0-255 scale and divide
// it out of RGB values

for (var i = 0; i < buffer.length; i += 4) {
var alpha = buffer[i + 3];
var norm = alpha / 255;

if (alpha === 0) {
buffer[i] = 0;
buffer[i + 1] = 0;
buffer[i + 2] = 0;
} else {
buffer[i] = buffer[i] / norm;
buffer[i + 1] = buffer[i + 1] / norm;
buffer[i + 2] = buffer[i + 2] / norm;
loadImages(images, map, function () {
map.render({
zoom: zoom,
center: center,
height: height,
width: width,
bearing: bearing,
pitch: pitch
}, function (err, buffer) {
if (err) {
console.error('Error rendering map');
console.error(err);
return reject(err);
}
} // Convert raw image buffer to PNG


try {
return (0, _sharp["default"])(buffer, {
raw: {
width: width * ratio,
height: height * ratio,
channels: 4
map.release(); // release map resources to prevent reusing in future render requests
// Un-premultiply pixel values
// Mapbox GL buffer contains premultiplied values, which are not handled correctly by sharp
// https://github.com/mapbox/mapbox-gl-native/issues/9124
// since we are dealing with 8-bit RGBA values, normalize alpha onto 0-255 scale and divide
// it out of RGB values

for (var i = 0; i < buffer.length; i += 4) {
var alpha = buffer[i + 3];
var norm = alpha / 255;

if (alpha === 0) {
buffer[i] = 0;
buffer[i + 1] = 0;
buffer[i + 2] = 0;
} else {
buffer[i] = buffer[i] / norm;
buffer[i + 1] = buffer[i + 1] / norm;
buffer[i + 2] = buffer[i + 2] / norm;
}
}).png().toBuffer().then(resolve)["catch"](reject);
} catch (pngErr) {
console.error('Error encoding PNG');
console.error(pngErr);
return reject(pngErr);
}
} // Convert raw image buffer to PNG


try {
return (0, _sharp["default"])(buffer, {
raw: {
width: width * ratio,
height: height * ratio,
channels: 4
}
}).png().toBuffer().then(resolve)["catch"](reject);
} catch (pngErr) {
console.error('Error encoding PNG');
console.error(pngErr);
return reject(pngErr);
}
});
});
});
};
Expand Down
Loading