Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for downloading remote images for icon-image handling #65

Closed
wants to merge 3 commits into from
Closed
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
185 changes: 137 additions & 48 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 @@ -367,6 +373,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$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (!(images !== null)) {
_context.next = 18;
break;
}

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

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

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

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

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

case 16:
_context.next = 2;
break;

case 18:
callback();

case 19:
case "end":
return _context.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 +484,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 +674,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
37 changes: 35 additions & 2 deletions dist/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports["default"] = exports.server = void 0;

var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));

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

var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
Expand All @@ -24,6 +26,10 @@ var _commander = _interopRequireDefault(require("commander"));

var _morgan = _interopRequireDefault(require("morgan"));

var _validUrl = _interopRequireDefault(require("valid-url"));

var _path = _interopRequireDefault(require("path"));

var _package = require("../package.json");

var _render = require("./render");
Expand Down Expand Up @@ -77,6 +83,10 @@ var PARAMS = {
token: {
isRequired: false,
isString: true
},
images: {
isRequired: false,
isObject: true
}
};

Expand All @@ -99,7 +109,9 @@ var renderImage = function renderImage(params, response, next, tilePath) {
_params$bounds = params.bounds,
bounds = _params$bounds === void 0 ? null : _params$bounds,
_params$ratio = params.ratio,
ratio = _params$ratio === void 0 ? 1 : _params$ratio;
ratio = _params$ratio === void 0 ? 1 : _params$ratio,
_params$images = params.images,
images = _params$images === void 0 ? null : _params$images;

if (typeof style === 'string') {
try {
Expand Down Expand Up @@ -225,6 +237,26 @@ var renderImage = function renderImage(params, response, next, tilePath) {
return next(new _restifyErrors["default"].BadRequestError('Either center and zoom OR bounds must be provided'));
}

if (images !== null) {
if (typeof images === 'string') {
images = JSON.parse(images);
} else if ((0, _typeof2["default"])(images) !== 'object') {
return next(new _restifyErrors["default"].BadRequestError('Images must be an object or a string'));
}

for (var imageName in images) {
var url = images[imageName];

if (!_validUrl["default"].isUri(url)) {
return next(new _restifyErrors["default"].BadRequestError("All images must be valid urls. ".concat(url, " is invalid")));
}

if (_path["default"].extname(imageName) !== '.png') {
return next(new _restifyErrors["default"].BadRequestError("Only png images are supported for now"));
}
}
}

try {
(0, _render.render)(style, parseInt(width, 10), parseInt(height, 10), {
zoom: zoom,
Expand All @@ -235,7 +267,8 @@ var renderImage = function renderImage(params, response, next, tilePath) {
ratio: ratio,
bearing: bearing,
pitch: pitch,
token: token
token: token,
images: images
}).then(function (data, rejected) {
if (rejected) {
console.error('render request rejected', rejected);
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ COPY package*.json /app/

RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install \
build-essential \
libcairo2-dev \
libgles2-mesa-dev \
libgbm-dev \
Expand Down
Loading