CartoDB/Windshaft

View on GitHub
lib/renderers/blend/factory.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

const mapnik = require('@carto/mapnik');
const Renderer = require('./renderer');
const BaseAdaptor = require('../base-adaptor');
const layersFilter = require('../../utils/layers-filter');

const EMPTY_IMAGE_BUFFER = new mapnik.Image(256, 256).encodeSync('png');

module.exports = class BlendFactory {
    static get NAME () {
        return 'blend';
    }

    constructor (rendererFactory) {
        this.rendererFactory = rendererFactory;
    }

    getName () {
        return BlendFactory.NAME;
    }

    supportsFormat (format) {
        return format === 'png';
    }

    getAdaptor (renderer, onTileErrorStrategy) {
        return new BaseAdaptor(renderer, onTileErrorStrategy);
    }

    getRenderer (mapConfig, format, options, callback) {
        const layer = options.layer;
        const params = options.params;
        const limits = options.limits;

        let filteredLayers;
        try {
            filteredLayers = layersFilter(mapConfig, layer);
        } catch (err) {
            return callback(err);
        }

        if (!filteredLayers.length) {
            return callback(new Error('No renderers'));
        }

        let hasMapnikLayer = false;
        const layerRenderers = filteredLayers.map(layerIndex => {
            return new Promise((resolve, reject) => {
                const rendererParams = Object.assign({}, params, { layer: layerIndex });
                const layerType = mapConfig.layerType(layerIndex);
                const isMapnikLayer = layerType === 'mapnik';

                if (isMapnikLayer && hasMapnikLayer) {
                    return resolve();
                }

                if (isMapnikLayer) {
                    rendererParams.layer = getMapnikLayersFromFilter(mapConfig, filteredLayers);
                }

                const rendererOptions = { limits };

                // Avoid failing because of timeouts or 404/errors in http layers
                rendererOptions.onTileErrorStrategy = layerType === 'http'
                    ? onTileErrorStrategyHttpRenderer
                    : onTileErrorStrategy;

                if (isMapnikLayer) {
                    hasMapnikLayer = true;
                }

                this.rendererFactory.getRenderer(mapConfig, rendererParams, rendererOptions, (err, renderer) => {
                    if (err) {
                        return reject(err);
                    }

                    return resolve(renderer);
                });
            });
        });

        return Promise.all(layerRenderers)
            .then(renderers => {
                if (!renderers.length) {
                    throw new Error('No renderers');
                }

                renderers = renderers.filter(renderer => !!renderer);

                return callback(null, new Renderer(renderers));
            })
            .catch(err => callback(err));
    }
};

async function onTileErrorStrategyHttpRenderer (err, format) {
    if (err.code === 'ETIMEDOUT') {
        throw err;
    }
    return { buffer: EMPTY_IMAGE_BUFFER, headers: { 'Content-Type': 'image/png' }, stats: {} };
}

async function onTileErrorStrategy (err, format) {
    if (err.message && err.message.match(/coordinates out of range/i)) {
        return { buffer: EMPTY_IMAGE_BUFFER, headers: { 'Content-Type': 'image/png' }, stats: {} };
    }
    throw err;
}

function getMapnikLayersFromFilter (mapConfig, filteredLayers) {
    return filteredLayers.filter(layerIndex => mapConfig.layerType(layerIndex) === 'mapnik').join(',');
}