CartoDB/Windshaft

View on GitHub
lib/models/mapconfig.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

const crypto = require('crypto');
const semver = require('semver');
const Datasource = require('./datasource');

// see: http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification
module.exports = class MapConfig {
    // Factory like method to create MapConfig objects when you are unsure about being
    // able to provide all the MapConfig collaborators or you have to create a MapConfig
    // object from a serialized version
    static create (rawConfig, datasource) {
        if (rawConfig.ds) {
            return new MapConfig(rawConfig.cfg, new Datasource(rawConfig.ds));
        }
        datasource = datasource || Datasource.EmptyDatasource();
        return new MapConfig(rawConfig, datasource);
    }

    static getLayerId (rawMapConfig, layerIndex) {
        const layer = rawMapConfig.layers[layerIndex];

        if (layer.id) {
            return layer.id;
        }

        const layerType = getType(layer.type);

        let layerId = `layer${getLayerIndexByType(rawMapConfig, layerType, layerIndex)}`;
        if (layerType !== 'mapnik') {
            layerId = `${layerType}-${layerId}`;
        }

        return layerId;
    }

    constructor (config, datasource) {
        // TODO: inject defaults ?
        this._id = null;
        this._cfg = config;
        this._datasource = datasource;

        if (!semver.satisfies(this.version(), '>= 1.0.0 <= 1.8.0')) {
            throw new Error(`Unsupported layergroup configuration version ${this.version()}`);
        }

        if (!Object.prototype.hasOwnProperty.call(this._cfg, 'layers')) {
            throw new Error('Missing layers array from layergroup config');
        }

        this._cfg.layers.forEach((layer, index) => {
            if (!Object.prototype.hasOwnProperty.call(layer, 'options')) {
                throw new Error(`Missing options from layer ${index} of layergroup config`);
            }

            // NOTE: interactivity used to be a string as of version 1.0.0
            if (Array.isArray(layer.options.interactivity)) {
                layer.options.interactivity = layer.options.interactivity.join(',');
            }
        });

        if (this._cfg.buffersize) {
            Object.keys(this._cfg.buffersize).forEach(format => {
                if (this._cfg.buffersize[format] !== undefined && !Number.isFinite(this._cfg.buffersize[format])) {
                    throw new Error(`Buffer size of format "${format}" must be a number`);
                }
            });
        }
    }

    serialize () {
        if (this._datasource.isEmpty()) {
            return JSON.stringify(this._cfg);
        }

        return JSON.stringify({
            cfg: this._cfg,
            ds: this._datasource.obj()
        });
    }

    id () {
        if (this._id === null) {
            this._id = md5Hash(JSON.stringify(this._cfg));
        }

        return this._id;
    }

    obj () {
        return this._cfg;
    }

    version () {
        return this._cfg.version || '1.0.0';
    }

    setDbParams (dbParams) {
        this._cfg.dbparams = dbParams;
        this.flush();
    }

    // flush id so it gets recalculated
    flush () {
        this._id = null;
    }

    layerType (layerIndex) {
        const layer = this.getLayer(layerIndex);

        if (!layer) {
            return undefined;
        }

        return this.getType(layer.type);
    }

    getType (type) {
        return getType(type);
    }

    setBufferSize (bufferSize) {
        this._cfg.buffersize = bufferSize;
        this.flush();

        return this;
    }

    getBufferSize (format) {
        if (this._cfg.buffersize && isValidBufferSize(this._cfg.buffersize[format])) {
            return parseInt(this._cfg.buffersize[format], 10);
        }

        return undefined;
    }

    hasIncompatibleLayers () {
        return !this.isVectorOnlyMapConfig() && this.hasVectorLayer();
    }

    isVectorOnlyMapConfig () {
        const layers = this.getLayers();
        let isVectorOnlyMapConfig = false;

        if (!layers.length) {
            return isVectorOnlyMapConfig;
        }

        isVectorOnlyMapConfig = true;

        for (let index = 0; index < layers.length; index++) {
            if (!this.isVectorLayer(index)) {
                isVectorOnlyMapConfig = false;
                break;
            }
        }

        return isVectorOnlyMapConfig;
    }

    hasVectorLayer () {
        const layers = this.getLayers();
        let hasVectorLayer = false;

        for (let index = 0; index < layers.length; index++) {
            if (this.isVectorLayer(index)) {
                hasVectorLayer = true;
                break;
            }
        }

        return hasVectorLayer;
    }

    isVectorLayer (index) {
        const layer = this.getLayer(index);
        const type = getType(layer.type);
        const sql = this.getLayerOption(index, 'sql');
        const cartocss = this.getLayerOption(index, 'cartocss');
        const cartocssVersion = this.getLayerOption(index, 'cartocss_version');

        return type === 'mapnik' && typeof sql === 'string' && cartocss === undefined && cartocssVersion === undefined;
    }

    getLayerId (layerIndex) {
        return MapConfig.getLayerId(this._cfg, layerIndex);
    }

    getIndexByLayerId (layerId) {
        for (const [index, layer] of this.getLayers().entries()) {
            if (layer.id === layerId) {
                return index;
            }
        }

        return -1;
    }

    getLayer (layerIndex) {
        return this._cfg.layers[layerIndex];
    }

    getLayers () {
        return this._cfg.layers.map((_layer, layerIndex) => this.getLayer(layerIndex));
    }

    getLayerIndexByType (type, mapConfigLayerIndex) {
        return getLayerIndexByType(this._cfg, type, mapConfigLayerIndex);
    }

    getLayerOption (layerIndex, optionName, defaultValue) {
        const layer = this.getLayer(layerIndex);
        let layerOption = defaultValue;

        if (layer && Object.prototype.hasOwnProperty.call(layer.options, optionName)) {
            layerOption = layer.options[optionName];
        }

        return layerOption;
    }

    getLayerDatasource (layerIndex) {
        const datasource = this._datasource.getLayerDatasource(layerIndex) || {};
        const layerSrid = this.getLayerOption(layerIndex, 'srid');

        if (layerSrid) {
            datasource.srid = layerSrid;
        }

        return datasource;
    }

    getMVTExtents () {
        const layers = this.getLayers();
        const extent = getTileExtent(layers);
        const simplifyExtent = getSimplifyExtent(layers, extent);

        return { extent: extent || DEFAULT_EXTENT, simplify_extent: simplifyExtent || DEFAULT_SIMPLIFY_EXTENT };
    }
};

function md5Hash (s) {
    return crypto.createHash('md5').update(s, 'binary').digest('hex');
}

function getType (type) {
    // TODO: check validity of other types ?
    return (!type || type === 'cartodb') ? 'mapnik' : type;
}

function isValidBufferSize (value) {
    return Number.isFinite(parseInt(value, 10));
}

function getLayerIndexByType (rawMapConfig, type, mapConfigLayerIdx) {
    let typeLayerIndex = 0;
    const mapConfigToTypeLayers = {};

    rawMapConfig.layers.forEach(function (layer, layerIdx) {
        if (getType(layer.type) === type) {
            mapConfigToTypeLayers[layerIdx] = typeLayerIndex++;
        }
    });

    return mapConfigToTypeLayers[mapConfigLayerIdx];
}

const DEFAULT_EXTENT = 4096;
const DEFAULT_SIMPLIFY_EXTENT = 256;
// Accepted values between 1 and 2^31 -1 (DEFAULT_MAX_EXTENT)
const DEFAULT_MAX_EXTENT = 2147483647;
const DEFAULT_MIN_EXTENT = 1;

function checkRange (number, min, max) {
    return (!isNaN(number) && number >= min && number <= max);
}

// Checks all layers for a valid `vector_simplify_extent`
// Makes sure all layers have the same value (or using DEFAULT_EXTENT)
// Returns undefined if none of the layers have it declared
function getSimplifyExtent (layers, vectorExtent) {
    let undef = 0;
    const extents = [...new Set(layers.map(layer => {
        if (layer.options.vector_simplify_extent === undefined) {
            undef++;
            return layer.options.vector_extent || DEFAULT_SIMPLIFY_EXTENT;
        }
        return layer.options.vector_simplify_extent;
    }))];

    if (extents.length > 1) {
        throw new Error(`Multiple simplify extent values in mapConfig (${extents})`);
    }

    if (undef === layers.length) {
        return vectorExtent;
    }

    const maxExtent = vectorExtent || DEFAULT_EXTENT;

    // Accepted values between 1 and max_extent
    const simplifyExtent = parseInt(extents[0]);
    if (!checkRange(simplifyExtent, DEFAULT_MIN_EXTENT, maxExtent)) {
        throw new Error(`Invalid vector_simplify_extent (${simplifyExtent}). Must be between 1 and vector_extent [${maxExtent}]`);
    }

    return simplifyExtent;
}

// Checks all layers for a valid `vectorExtent`
// Makes sure all layers have the same value (or using DEFAULT_EXTENT)
// Returns undefined if none of the layers have it declared
function getTileExtent (layers) {
    let undef = 0;
    const layerExtents = [...new Set(layers.map(layer => {
        if (layer.options.vector_extent === undefined) {
            undef++;
            return DEFAULT_EXTENT;
        }
        return layer.options.vector_extent;
    }))];

    if (layerExtents.length > 1) {
        throw new Error(`Multiple extent values in mapConfig (${layerExtents})`);
    }

    if (undef === layers.length) {
        return undefined;
    }

    const extent = parseInt(layerExtents[0]);
    if (!checkRange(extent, DEFAULT_MIN_EXTENT, DEFAULT_MAX_EXTENT)) {
        throw new Error(`Invalid vector_extent. Must be between 1 and ${DEFAULT_MAX_EXTENT}`);
    }

    return extent;
}