<commit_before>const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const cors = require('../middleware/cors');
const userMiddleware = require('../middleware/user');
const allowQueryParams = require('../middleware/allow-query-params');
const vectorError = require('../middleware/vector-error');

const DEFAULT_ZOOM_CENTER = {
    zoom: 1,
    center: {
        lng: 0,
        lat: 0
    }
};

function numMapper(n) {
    return +n;
}

function getRequestParams(locals) {
    const params = Object.assign({}, locals);

    delete params.template;
    delete params.affectedTablesAndLastUpdate;
    delete params.namedMapProvider;
    delete params.allowedQueryParams;

    return params;
}

function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
                             surrogateKeysCache, tablesExtentApi, metadataBackend) {
    this.namedMapProviderCache = namedMapProviderCache;
    this.tileBackend = tileBackend;
    this.previewBackend = previewBackend;
    this.surrogateKeysCache = surrogateKeysCache;
    this.tablesExtentApi = tablesExtentApi;
    this.metadataBackend = metadataBackend;
    this.prepareContext = prepareContext;
}

module.exports = NamedMapsController;

NamedMapsController.prototype.register = function(app) {
    const tileOptions = {
        label: 'NAMED_MAP_TILE'
    };

    app.get(
        app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
        cors(),
        userMiddleware,
        this.prepareContext,
        this.getNamedMapProvider(tileOptions),
        this.getAffectedTables(),
        this.getTile(tileOptions),
        this.setSurrogateKey(),
        this.setCacheChannelHeader(),
        this.setLastModifiedHeader(),
        this.setCacheControlHeader(),
        this.setContentTypeHeader(),
        this.respond(),
        vectorError()
    );

    const staticOptions = {
        forcedFormat: 'png',
        label: 'STATIC_VIZ_MAP'
    };

    app.get(
        app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
        cors(),
        userMiddleware,
        allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
        this.prepareContext,
        this.getNamedMapProvider(staticOptions),
        this.getAffectedTables(),
        this.getTemplate(staticOptions),
        this.prepareLayerFilterFromPreviewLayers(staticOptions),
        this.getStaticImageOptions(),
        this.getImage(staticOptions),
        this.incrementMapViews(),
        this.setSurrogateKey(),
        this.setCacheChannelHeader(),
        this.setLastModifiedHeader(),
        this.setCacheControlHeader(),
        this.setContentTypeHeader(),
        this.respond()
    );
};

NamedMapsController.prototype.getNamedMapProvider = function ({ label, forcedFormat }) {
    return function getNamedMapProviderMiddleware (req, res, next) {
        const { user } = res.locals;
        const { config, auth_token } = req.query;
        const { template_id } = req.params;

        if (forcedFormat) {
            res.locals.format = forcedFormat;
            res.locals.layer = res.locals.layer || 'all';
        }

        const params = getRequestParams(res.locals);

        this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.namedMapProvider = namedMapProvider;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getAffectedTables = function () {
    return function getAffectedTables (req, res, next) {
        const { namedMapProvider } = res.locals;

        namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
            req.profiler.done('affectedTables');

            if (err) {
                return next(err);
            }

            res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getTemplate = function ({ label }) {
    return function getTemplateMiddleware (req, res, next) {
        const { namedMapProvider } = res.locals;

        namedMapProvider.getTemplate((err, template) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.template = template;

            next();
        });
    };
};

NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function ({ label }) {
    return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
        const { user, template } = res.locals;
        const { template_id } = req.params;
        const { config, auth_token } = req.query;

        if (!template || !template.view || !template.view.preview_layers) {
            return next();
        }

        var previewLayers = template.view.preview_layers;
        var layerVisibilityFilter = [];

        template.layergroup.layers.forEach((layer, index) => {
            if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
                layerVisibilityFilter.push(''+index);
            }
        });

        if (!layerVisibilityFilter.length) {
            return next();
        }

        const params = getRequestParams(res.locals);

        // overwrites 'all' default filter
        params.layer = layerVisibilityFilter.join(',');

        // recreates the provider
        this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.namedMapProvider = provider;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getTile = function ({ label }) {
    return function getTileMiddleware (req, res, next) {
        const { namedMapProvider } = res.locals;

        this.tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => {
            req.profiler.add(stats);

            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.body = tile;
            res.locals.headers = headers;
            res.locals.stats = stats;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getStaticImageOptions = function () {
    return function getStaticImageOptionsMiddleware(req, res, next) {
        const { user, namedMapProvider, template } = res.locals;

        const imageOpts = getImageOptions(res.locals, template);

        if (imageOpts) {
            res.locals.imageOpts = imageOpts;
            return next();
        }

        res.locals.imageOpts = DEFAULT_ZOOM_CENTER;

        namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
            if (err) {
                return next();
            }

            var affectedTables = affectedTablesAndLastUpdate.tables || [];

            if (affectedTables.length === 0) {
                return next();
            }

            this.tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
                if (err) {
                    return next();
                }

                res.locals.imageOpts = bounds;

                return next();
            });
        });
    }.bind(this);
};

function getImageOptions (params, template) {
    const { zoom, lon, lat, bbox } = params;

    let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
    if (imageOpts) {
        return imageOpts;
    }

    imageOpts = getImageOptionsFromBoundingBox(bbox);
    if (imageOpts) {
        return imageOpts;
    }

    imageOpts = getImageOptionsFromTemplate(template, zoom);
    if (imageOpts) {
        return imageOpts;
    }
}

function getImageOptionsFromCoordinates (zoom, lon, lat) {
    if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
        return {
            zoom: zoom,
            center: {
                lng: lon,
                lat: lat
            }
        };
    }
}

function getImageOptionsFromTemplate (template, zoom) {
    if (template.view) {
        var zoomCenter = templateZoomCenter(template.view);
        if (zoomCenter) {
            if (Number.isFinite(+zoom)) {
                zoomCenter.zoom = +zoom;
            }

            return zoomCenter;
        }

        var bounds = templateBounds(template.view);
        if (bounds) {
            return bounds;
        }
    }
}

function getImageOptionsFromBoundingBox (bbox = '') {
    var _bbox = bbox.split(',').map(numMapper);

    if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
        return {
            bounds: {
                west: _bbox[0],
                south: _bbox[1],
                east: _bbox[2],
                north: _bbox[3]
            }
        };
    }
}

NamedMapsController.prototype.getImage = function ({ label }) {
    return function getImageMiddleware (req, res, next) {
        const { imageOpts, namedMapProvider } = res.locals;
        const { zoom, center, bounds } = imageOpts;

        let { width, height } = req.params;

        width = +width;
        height = +height;

        const format = req.params.format === 'jpg' ? 'jpeg' : 'png';

        if (zoom !== undefined && center) {
            return this.previewBackend.getImage(namedMapProvider, format, width, height, zoom, center,
                (err, image, headers, stats) => {
                if (err) {
                    err.label = label;
                    return next(err);
                }

                res.locals.body = image;
                res.locals.headers = headers;
                res.locals.stats = stats;

                next();
            });
        }

        this.previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.body = image;
            res.locals.headers = headers;
            res.locals.stats = stats;

            next();
        });
    }.bind(this);
};

function incrementMapViewsError (ctx) {
    return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
}

NamedMapsController.prototype.incrementMapViews = function () {
    return function incrementMapViewsMiddleware(req, res, next) {
        const { user, namedMapProvider } = res.locals;

        namedMapProvider.getMapConfig((err, mapConfig) => {
            if (err) {
                global.logger.log(incrementMapViewsError({ user, err }));
                return next();
            }

            const statTag = mapConfig.obj().stat_tag;

            this.metadataBackend.incMapviewCount(user, statTag, (err) => {
                if (err) {
                    global.logger.log(incrementMapViewsError({ user, err }));
                }

                next();
            });
        });
    }.bind(this);
};

function templateZoomCenter(view) {
    if (view.zoom !== undefined && view.center) {
        return {
            zoom: view.zoom,
            center: view.center
        };
    }
    return false;
}

function templateBounds(view) {
    if (view.bounds) {
        var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));

        if (hasAllBounds) {
            return {
                bounds: {
                    west: view.bounds.west,
                    south: view.bounds.south,
                    east: view.bounds.east,
                    north: view.bounds.north
                }
            };
        } else {
            return false;
        }
    }
    return false;
}

NamedMapsController.prototype.setCacheChannelHeader = function () {
    return function setCacheChannelHeaderMiddleware (req, res, next) {
        const { affectedTablesAndLastUpdate } = res.locals;

        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
        }

        next();
    };
};

NamedMapsController.prototype.setSurrogateKey = function () {
    return function setSurrogateKeyMiddleware(req, res, next) {
        const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;

        this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            if (affectedTablesAndLastUpdate.tables.length > 0) {
                this.surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
            }
        }

        next();
    }.bind(this);
};

NamedMapsController.prototype.setLastModifiedHeader = function () {
    return function setLastModifiedHeaderMiddleware(req, res, next) {
        const { affectedTablesAndLastUpdate } = res.locals;

        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            var lastModifiedDate;
            if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
                lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
            } else {
                lastModifiedDate = new Date();
            }

            res.set('Last-Modified', lastModifiedDate.toUTCString());
        }

        next();
    };
 };

NamedMapsController.prototype.setCacheControlHeader = function () {
    return function setCacheControlHeaderMiddleware(req, res, next) {
        const { affectedTablesAndLastUpdate } = res.locals;

        res.set('Cache-Control', 'public,max-age=7200,must-revalidate');

        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            // we increase cache control as we can invalidate it
            res.set('Cache-Control', 'public,max-age=31536000');
        }

        next();
    };
 };

NamedMapsController.prototype.setContentTypeHeader = function () {
    return function setContentTypeHeaderMiddleware(req, res, next) {
        const { headers = {} } = res.locals;

        res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');

        next();
    };
};

NamedMapsController.prototype.respond = function () {
    return function respondMiddleware (req, res) {
        const { body, stats = {}, format } = res.locals;

        req.profiler.done('render-' + format);
        req.profiler.add(stats);

        res.status(200);
        res.send(body);
    };
};
<commit_msg>Make explicit that `forceFormat` is optional<commit_after>const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const cors = require('../middleware/cors');
const userMiddleware = require('../middleware/user');
const allowQueryParams = require('../middleware/allow-query-params');
const vectorError = require('../middleware/vector-error');

const DEFAULT_ZOOM_CENTER = {
    zoom: 1,
    center: {
        lng: 0,
        lat: 0
    }
};

function numMapper(n) {
    return +n;
}

function getRequestParams(locals) {
    const params = Object.assign({}, locals);

    delete params.template;
    delete params.affectedTablesAndLastUpdate;
    delete params.namedMapProvider;
    delete params.allowedQueryParams;

    return params;
}

function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
                             surrogateKeysCache, tablesExtentApi, metadataBackend) {
    this.namedMapProviderCache = namedMapProviderCache;
    this.tileBackend = tileBackend;
    this.previewBackend = previewBackend;
    this.surrogateKeysCache = surrogateKeysCache;
    this.tablesExtentApi = tablesExtentApi;
    this.metadataBackend = metadataBackend;
    this.prepareContext = prepareContext;
}

module.exports = NamedMapsController;

NamedMapsController.prototype.register = function(app) {
    const tileOptions = {
        label: 'NAMED_MAP_TILE'
    };

    app.get(
        app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
        cors(),
        userMiddleware,
        this.prepareContext,
        this.getNamedMapProvider(tileOptions),
        this.getAffectedTables(),
        this.getTile(tileOptions),
        this.setSurrogateKey(),
        this.setCacheChannelHeader(),
        this.setLastModifiedHeader(),
        this.setCacheControlHeader(),
        this.setContentTypeHeader(),
        this.respond(),
        vectorError()
    );

    const staticOptions = {
        forcedFormat: 'png',
        label: 'STATIC_VIZ_MAP'
    };

    app.get(
        app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
        cors(),
        userMiddleware,
        allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
        this.prepareContext,
        this.getNamedMapProvider(staticOptions),
        this.getAffectedTables(),
        this.getTemplate(staticOptions),
        this.prepareLayerFilterFromPreviewLayers(staticOptions),
        this.getStaticImageOptions(),
        this.getImage(staticOptions),
        this.incrementMapViews(),
        this.setSurrogateKey(),
        this.setCacheChannelHeader(),
        this.setLastModifiedHeader(),
        this.setCacheControlHeader(),
        this.setContentTypeHeader(),
        this.respond()
    );
};

NamedMapsController.prototype.getNamedMapProvider = function ({ label, forcedFormat = null }) {
    return function getNamedMapProviderMiddleware (req, res, next) {
        const { user } = res.locals;
        const { config, auth_token } = req.query;
        const { template_id } = req.params;

        if (forcedFormat) {
            res.locals.format = forcedFormat;
            res.locals.layer = res.locals.layer || 'all';
        }

        const params = getRequestParams(res.locals);

        this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.namedMapProvider = namedMapProvider;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getAffectedTables = function () {
    return function getAffectedTables (req, res, next) {
        const { namedMapProvider } = res.locals;

        namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
            req.profiler.done('affectedTables');

            if (err) {
                return next(err);
            }

            res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getTemplate = function ({ label }) {
    return function getTemplateMiddleware (req, res, next) {
        const { namedMapProvider } = res.locals;

        namedMapProvider.getTemplate((err, template) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.template = template;

            next();
        });
    };
};

NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function ({ label }) {
    return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
        const { user, template } = res.locals;
        const { template_id } = req.params;
        const { config, auth_token } = req.query;

        if (!template || !template.view || !template.view.preview_layers) {
            return next();
        }

        var previewLayers = template.view.preview_layers;
        var layerVisibilityFilter = [];

        template.layergroup.layers.forEach((layer, index) => {
            if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
                layerVisibilityFilter.push(''+index);
            }
        });

        if (!layerVisibilityFilter.length) {
            return next();
        }

        const params = getRequestParams(res.locals);

        // overwrites 'all' default filter
        params.layer = layerVisibilityFilter.join(',');

        // recreates the provider
        this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.namedMapProvider = provider;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getTile = function ({ label }) {
    return function getTileMiddleware (req, res, next) {
        const { namedMapProvider } = res.locals;

        this.tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => {
            req.profiler.add(stats);

            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.body = tile;
            res.locals.headers = headers;
            res.locals.stats = stats;

            next();
        });
    }.bind(this);
};

NamedMapsController.prototype.getStaticImageOptions = function () {
    return function getStaticImageOptionsMiddleware(req, res, next) {
        const { user, namedMapProvider, template } = res.locals;

        const imageOpts = getImageOptions(res.locals, template);

        if (imageOpts) {
            res.locals.imageOpts = imageOpts;
            return next();
        }

        res.locals.imageOpts = DEFAULT_ZOOM_CENTER;

        namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
            if (err) {
                return next();
            }

            var affectedTables = affectedTablesAndLastUpdate.tables || [];

            if (affectedTables.length === 0) {
                return next();
            }

            this.tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
                if (err) {
                    return next();
                }

                res.locals.imageOpts = bounds;

                return next();
            });
        });
    }.bind(this);
};

function getImageOptions (params, template) {
    const { zoom, lon, lat, bbox } = params;

    let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
    if (imageOpts) {
        return imageOpts;
    }

    imageOpts = getImageOptionsFromBoundingBox(bbox);
    if (imageOpts) {
        return imageOpts;
    }

    imageOpts = getImageOptionsFromTemplate(template, zoom);
    if (imageOpts) {
        return imageOpts;
    }
}

function getImageOptionsFromCoordinates (zoom, lon, lat) {
    if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
        return {
            zoom: zoom,
            center: {
                lng: lon,
                lat: lat
            }
        };
    }
}

function getImageOptionsFromTemplate (template, zoom) {
    if (template.view) {
        var zoomCenter = templateZoomCenter(template.view);
        if (zoomCenter) {
            if (Number.isFinite(+zoom)) {
                zoomCenter.zoom = +zoom;
            }

            return zoomCenter;
        }

        var bounds = templateBounds(template.view);
        if (bounds) {
            return bounds;
        }
    }
}

function getImageOptionsFromBoundingBox (bbox = '') {
    var _bbox = bbox.split(',').map(numMapper);

    if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
        return {
            bounds: {
                west: _bbox[0],
                south: _bbox[1],
                east: _bbox[2],
                north: _bbox[3]
            }
        };
    }
}

NamedMapsController.prototype.getImage = function ({ label }) {
    return function getImageMiddleware (req, res, next) {
        const { imageOpts, namedMapProvider } = res.locals;
        const { zoom, center, bounds } = imageOpts;

        let { width, height } = req.params;

        width = +width;
        height = +height;

        const format = req.params.format === 'jpg' ? 'jpeg' : 'png';

        if (zoom !== undefined && center) {
            return this.previewBackend.getImage(namedMapProvider, format, width, height, zoom, center,
                (err, image, headers, stats) => {
                if (err) {
                    err.label = label;
                    return next(err);
                }

                res.locals.body = image;
                res.locals.headers = headers;
                res.locals.stats = stats;

                next();
            });
        }

        this.previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => {
            if (err) {
                err.label = label;
                return next(err);
            }

            res.locals.body = image;
            res.locals.headers = headers;
            res.locals.stats = stats;

            next();
        });
    }.bind(this);
};

function incrementMapViewsError (ctx) {
    return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
}

NamedMapsController.prototype.incrementMapViews = function () {
    return function incrementMapViewsMiddleware(req, res, next) {
        const { user, namedMapProvider } = res.locals;

        namedMapProvider.getMapConfig((err, mapConfig) => {
            if (err) {
                global.logger.log(incrementMapViewsError({ user, err }));
                return next();
            }

            const statTag = mapConfig.obj().stat_tag;

            this.metadataBackend.incMapviewCount(user, statTag, (err) => {
                if (err) {
                    global.logger.log(incrementMapViewsError({ user, err }));
                }

                next();
            });
        });
    }.bind(this);
};

function templateZoomCenter(view) {
    if (view.zoom !== undefined && view.center) {
        return {
            zoom: view.zoom,
            center: view.center
        };
    }
    return false;
}

function templateBounds(view) {
    if (view.bounds) {
        var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));

        if (hasAllBounds) {
            return {
                bounds: {
                    west: view.bounds.west,
                    south: view.bounds.south,
                    east: view.bounds.east,
                    north: view.bounds.north
                }
            };
        } else {
            return false;
        }
    }
    return false;
}

NamedMapsController.prototype.setCacheChannelHeader = function () {
    return function setCacheChannelHeaderMiddleware (req, res, next) {
        const { affectedTablesAndLastUpdate } = res.locals;

        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
        }

        next();
    };
};

NamedMapsController.prototype.setSurrogateKey = function () {
    return function setSurrogateKeyMiddleware(req, res, next) {
        const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;

        this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            if (affectedTablesAndLastUpdate.tables.length > 0) {
                this.surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
            }
        }

        next();
    }.bind(this);
};

NamedMapsController.prototype.setLastModifiedHeader = function () {
    return function setLastModifiedHeaderMiddleware(req, res, next) {
        const { affectedTablesAndLastUpdate } = res.locals;

        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            var lastModifiedDate;
            if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
                lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
            } else {
                lastModifiedDate = new Date();
            }

            res.set('Last-Modified', lastModifiedDate.toUTCString());
        }

        next();
    };
 };

NamedMapsController.prototype.setCacheControlHeader = function () {
    return function setCacheControlHeaderMiddleware(req, res, next) {
        const { affectedTablesAndLastUpdate } = res.locals;

        res.set('Cache-Control', 'public,max-age=7200,must-revalidate');

        if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
            // we increase cache control as we can invalidate it
            res.set('Cache-Control', 'public,max-age=31536000');
        }

        next();
    };
 };

NamedMapsController.prototype.setContentTypeHeader = function () {
    return function setContentTypeHeaderMiddleware(req, res, next) {
        const { headers = {} } = res.locals;

        res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');

        next();
    };
};

NamedMapsController.prototype.respond = function () {
    return function respondMiddleware (req, res) {
        const { body, stats = {}, format } = res.locals;

        req.profiler.done('render-' + format);
        req.profiler.add(stats);

        res.status(200);
        res.send(body);
    };
};
<|endoftext|>


<commit_before>import lonlat from '@conveyal/lonlat'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { FormGroup, FormControl, InputGroup, DropdownButton, MenuItem } from 'react-bootstrap'
import { connect } from 'react-redux'
import { autocomplete } from 'isomorphic-mapzen-search'

import { setLocation, setLocationToCurrent, clearLocation } from '../../actions/map'
import { addLocationSearch } from '../../actions/location'
import { distanceStringImperial } from '../../util/distance'

class LocationField extends Component {
  static propTypes = {
    config: PropTypes.object,
    currentPosition: PropTypes.object,
    location: PropTypes.object,
    label: PropTypes.string,
    nearbyStops: PropTypes.array,
    sessionSearches: PropTypes.array,
    static: PropTypes.bool,
    stopsIndex: PropTypes.object,
    type: PropTypes.string, // replace with locationType?

    // callbacks
    onClick: PropTypes.func,

    // dispatch
    addLocationSearch: PropTypes.func,
    clearLocation: PropTypes.func,
    setLocation: PropTypes.func,
    setLocationToCurrent: PropTypes.func
  }

  constructor (props) {
    super(props)
    this.state = {
      value: props.location !== null ? props.location.name : '',
      menuVisible: false,
      geocodedFeatures: []
    }
  }

  componentWillReceiveProps (nextProps) {
    if (this.props.location !== nextProps.location) {
      this.setState({
        value: nextProps.location !== null ? nextProps.location.name : '',
        geocodedFeatures: []
      })
    }
  }

  componentDidMount () {
    if (this.props.static) {
      ReactDOM.findDOMNode(this.refs['formControl']).focus()
    }
  }

  _geocode (text) {
    const { config } = this.props
    autocomplete({
      apiKey: config.geocoder.MAPZEN_KEY,
      boundary: config.geocoder.boundary,
      // TODO: use current location as focus point
      text
    }).then((result) => {
      console.log(result)
      this.setState({ geocodedFeatures: result.features })
    }).catch((err) => {
      console.error(err)
    })
  }

  _setLocation (location) {
    this.props.setLocation(this.props.type, location)
  }

  render () {
    const { currentPosition } = this.props

    const currentLocationOption = currentPosition !== null
      ? createOption('location-arrow', 'Use Current Location', () => {
        this.props.setLocationToCurrent(this.props.type)
      })
      : null

    let geocodedFeatures = this.state.geocodedFeatures
    if (geocodedFeatures.length > 5) geocodedFeatures = geocodedFeatures.slice(0, 5)

    let nearbyStops = this.props.nearbyStops
    if (nearbyStops.length > 4) nearbyStops = nearbyStops.slice(0, 3)

    let sessionSearches = this.props.sessionSearches
    if (sessionSearches.length > 5) sessionSearches = sessionSearches.slice(0, 5)

    const formControlClassname = this.props.type + '-form-control'

    // assemble menu contents, to be displayed either as dropdown or static panel
    let menuItems = []
    menuItems.push(currentLocationOption)

    // geocode search result option(s)
    if (geocodedFeatures.length > 0) {
      menuItems.push(<MenuItem header key='sr-header'>Search Results</MenuItem>)
      menuItems = menuItems.concat(geocodedFeatures.map(feature => {
        return createOption('map-pin', feature.properties.label, () => {
          const location = lonlat.fromCoordinates(feature.geometry.coordinates)
          location.name = feature.properties.label

          // set the current location
          this._setLocation(location)

          // add to the location search history
          this.props.addLocationSearch(location)
        })
      }))
    }

    // nearby transit stop options
    if (nearbyStops.length > 0) {
      menuItems.push(<MenuItem header key='ns-header'>Nearby Stops</MenuItem>)
      menuItems = menuItems.concat(nearbyStops.map(stopId => {
        const stop = this.props.stopsIndex[stopId]
        return createTransitStopOption(stop, () => {
          this._setLocation({
            name: stop.name,
            lat: stop.lat,
            lon: stop.lon
          })
        })
      }))
    }

    // recent search history options
    if (sessionSearches.length > 0) {
      menuItems.push(<MenuItem header key='ss-header'>Recently Searched</MenuItem>)
      menuItems = menuItems.concat(sessionSearches.map(location => {
        return createOption('search', location.name, () => {
          // set the current location
          this._setLocation(location)
        })
      }))
    }

    /** the text input element **/
    const textControl = <FormControl ref='formControl'
      className={formControlClassname}
      type='text'
      value={this.state.value}
      placeholder={this.props.label || this.props.type}
      onChange={(e) => {
        this.setState({ value: e.target.value })
        this._geocode(e.target.value)
      }}
      onClick={() => {
        if (typeof this.props.onClick === 'function') this.props.onClick()
        this.setState({ menuVisible: true })
      }}
    />

    /** the clear ('X') button add-on */
    const clearButton = <InputGroup.Addon onClick={() => {
      this.props.clearLocation(this.props.type)
      this.setState({
        value: '',
        geocodedFeatures: []
      })
    }}>

    </InputGroup.Addon>

    if (this.props.static) { // 'static' mode (menu is displayed alongside input)
      return (

          <form>
            <FormGroup>
              <InputGroup>
                <InputGroup.Addon>
                  <i className={`fa fa-${this.props.type === 'from' ? 'star' : 'map-marker'}`} />
                </InputGroup.Addon>
                {textControl}
                {clearButton}
              </InputGroup>
            </FormGroup>
          </form>

            {menuItems}

      )
    } else { // default display mode with dropdown menu
      return (
        <form>
          <FormGroup className='location-field'>
            <InputGroup>
              {/* location field icon -- also serves as dropdown anchor */}
              <DropdownButton
                componentClass={InputGroup.Button}
                open={this.state.menuVisible}
                onToggle={(v, e) => {
                  // if clicked on input form control, keep dropdown open; otherwise, toggle
                  const targetIsInput = e.target.className.indexOf(formControlClassname) !== -1
                  this.setState({ menuVisible: targetIsInput ? true : !this.state.menuVisible })
                }}
                id='location-dropdown'
                title={<i className={`fa fa-${this.props.type === 'from' ? 'star' : 'map-marker'}`} />}
                noCaret
              >
                {menuItems}
              </DropdownButton>
              {textControl}
              {clearButton}
            </InputGroup>
          </FormGroup>
        </form>
      )
    }
  }
}

// helper functions for dropdown options

let itemKey = 0

function createOption (icon, title, onSelect) {
  return <MenuItem className='location-option' onSelect={onSelect} key={itemKey++}>

<i className={`fa fa-${icon}`} />

{title}

  </MenuItem>
}

function createTransitStopOption (stop, onSelect) {
  return <MenuItem className='location-option' onSelect={onSelect} key={itemKey++}>

{distanceStringImperial(stop.dist, true)}

{stop.name} ({stop.code})

          {(stop.routes || []).map((route, i) => {
            const name = route.shortName || route.longName
            return (
              <span key={`route-${i}`} className='route'>
                {name}

            )
          })}

  </MenuItem>
}

// connect to redux store

const mapStateToProps = (state, ownProps) => {
  return {
    config: state.otp.config,
    location: state.otp.currentQuery[ownProps.type],
    currentPosition: state.otp.location.currentPosition,
    sessionSearches: state.otp.location.sessionSearches,
    nearbyStops: state.otp.location.nearbyStops,
    stopsIndex: state.otp.transitIndex.stops
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    addLocationSearch: (location) => { dispatch(addLocationSearch({ location })) },
    setLocation: (type, location) => { dispatch(setLocation({ type, location })) },
    setLocationToCurrent: (type) => { dispatch(setLocationToCurrent({ type })) },
    clearLocation: (type) => { dispatch(clearLocation({ type })) }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(LocationField)
<commit_msg>feat(form): Allow configuration of whether LocationField includes clear ('x') button<commit_after>import lonlat from '@conveyal/lonlat'
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { FormGroup, FormControl, InputGroup, DropdownButton, MenuItem } from 'react-bootstrap'
import { connect } from 'react-redux'
import { autocomplete } from 'isomorphic-mapzen-search'

import { setLocation, setLocationToCurrent, clearLocation } from '../../actions/map'
import { addLocationSearch } from '../../actions/location'
import { distanceStringImperial } from '../../util/distance'

class LocationField extends Component {
  static propTypes = {
    config: PropTypes.object,
    currentPosition: PropTypes.object,
    location: PropTypes.object,
    label: PropTypes.string,
    nearbyStops: PropTypes.array,
    sessionSearches: PropTypes.array,
    showClearButton: PropTypes.bool,
    static: PropTypes.bool,
    stopsIndex: PropTypes.object,
    type: PropTypes.string, // replace with locationType?

    // callbacks
    onClick: PropTypes.func,

    // dispatch
    addLocationSearch: PropTypes.func,
    clearLocation: PropTypes.func,
    setLocation: PropTypes.func,
    setLocationToCurrent: PropTypes.func
  }

  static defaultProps = {
    showClearButton: true
  }

  constructor (props) {
    super(props)
    this.state = {
      value: props.location !== null ? props.location.name : '',
      menuVisible: false,
      geocodedFeatures: []
    }
  }

  componentWillReceiveProps (nextProps) {
    if (this.props.location !== nextProps.location) {
      this.setState({
        value: nextProps.location !== null ? nextProps.location.name : '',
        geocodedFeatures: []
      })
    }
  }

  componentDidMount () {
    if (this.props.static) {
      ReactDOM.findDOMNode(this.refs['formControl']).focus()
    }
  }

  _geocode (text) {
    const { config } = this.props
    autocomplete({
      apiKey: config.geocoder.MAPZEN_KEY,
      boundary: config.geocoder.boundary,
      // TODO: use current location as focus point
      text
    }).then((result) => {
      console.log(result)
      this.setState({ geocodedFeatures: result.features })
    }).catch((err) => {
      console.error(err)
    })
  }

  _setLocation (location) {
    this.props.setLocation(this.props.type, location)
  }

  render () {
    const { currentPosition } = this.props

    const currentLocationOption = currentPosition !== null
      ? createOption('location-arrow', 'Use Current Location', () => {
        this.props.setLocationToCurrent(this.props.type)
      })
      : null

    let geocodedFeatures = this.state.geocodedFeatures
    if (geocodedFeatures.length > 5) geocodedFeatures = geocodedFeatures.slice(0, 5)

    let nearbyStops = this.props.nearbyStops
    if (nearbyStops.length > 4) nearbyStops = nearbyStops.slice(0, 3)

    let sessionSearches = this.props.sessionSearches
    if (sessionSearches.length > 5) sessionSearches = sessionSearches.slice(0, 5)

    const formControlClassname = this.props.type + '-form-control'

    // assemble menu contents, to be displayed either as dropdown or static panel
    let menuItems = []
    menuItems.push(currentLocationOption)

    // geocode search result option(s)
    if (geocodedFeatures.length > 0) {
      menuItems.push(<MenuItem header key='sr-header'>Search Results</MenuItem>)
      menuItems = menuItems.concat(geocodedFeatures.map(feature => {
        return createOption('map-pin', feature.properties.label, () => {
          const location = lonlat.fromCoordinates(feature.geometry.coordinates)
          location.name = feature.properties.label

          // set the current location
          this._setLocation(location)

          // add to the location search history
          this.props.addLocationSearch(location)
        })
      }))
    }

    // nearby transit stop options
    if (nearbyStops.length > 0) {
      menuItems.push(<MenuItem header key='ns-header'>Nearby Stops</MenuItem>)
      menuItems = menuItems.concat(nearbyStops.map(stopId => {
        const stop = this.props.stopsIndex[stopId]
        return createTransitStopOption(stop, () => {
          this._setLocation({
            name: stop.name,
            lat: stop.lat,
            lon: stop.lon
          })
        })
      }))
    }

    // recent search history options
    if (sessionSearches.length > 0) {
      menuItems.push(<MenuItem header key='ss-header'>Recently Searched</MenuItem>)
      menuItems = menuItems.concat(sessionSearches.map(location => {
        return createOption('search', location.name, () => {
          // set the current location
          this._setLocation(location)
        })
      }))
    }

    /** the text input element **/
    const textControl = <FormControl ref='formControl'
      className={formControlClassname}
      type='text'
      value={this.state.value}
      placeholder={this.props.label || this.props.type}
      onChange={(e) => {
        this.setState({ value: e.target.value })
        this._geocode(e.target.value)
      }}
      onClick={() => {
        if (typeof this.props.onClick === 'function') this.props.onClick()
        this.setState({ menuVisible: true })
      }}
    />

    /** the clear ('X') button add-on */
    const clearButton = <InputGroup.Addon onClick={() => {
      this.props.clearLocation(this.props.type)
      this.setState({
        value: '',
        geocodedFeatures: []
      })
    }}>

    </InputGroup.Addon>

    if (this.props.static) { // 'static' mode (menu is displayed alongside input)
      return (

          <form>
            <FormGroup>
              <InputGroup>
                <InputGroup.Addon>
                  <i className={`fa fa-${this.props.type === 'from' ? 'star' : 'map-marker'}`} />
                </InputGroup.Addon>
                {textControl}
                {this.props.showClearButton && clearButton}
              </InputGroup>
            </FormGroup>
          </form>

            {menuItems}

      )
    } else { // default display mode with dropdown menu
      return (
        <form>
          <FormGroup className='location-field'>
            <InputGroup>
              {/* location field icon -- also serves as dropdown anchor */}
              <DropdownButton
                componentClass={InputGroup.Button}
                open={this.state.menuVisible}
                onToggle={(v, e) => {
                  // if clicked on input form control, keep dropdown open; otherwise, toggle
                  const targetIsInput = e.target.className.indexOf(formControlClassname) !== -1
                  this.setState({ menuVisible: targetIsInput ? true : !this.state.menuVisible })
                }}
                id='location-dropdown'
                title={<i className={`fa fa-${this.props.type === 'from' ? 'star' : 'map-marker'}`} />}
                noCaret
              >
                {menuItems}
              </DropdownButton>
              {textControl}
              {this.props.showClearButton && clearButton}
            </InputGroup>
          </FormGroup>
        </form>
      )
    }
  }
}

// helper functions for dropdown options

let itemKey = 0

function createOption (icon, title, onSelect) {
  return <MenuItem className='location-option' onSelect={onSelect} key={itemKey++}>

<i className={`fa fa-${icon}`} />

{title}

  </MenuItem>
}

function createTransitStopOption (stop, onSelect) {
  return <MenuItem className='location-option' onSelect={onSelect} key={itemKey++}>

{distanceStringImperial(stop.dist, true)}

{stop.name} ({stop.code})

          {(stop.routes || []).map((route, i) => {
            const name = route.shortName || route.longName
            return (
              <span key={`route-${i}`} className='route'>
                {name}

            )
          })}

  </MenuItem>
}

// connect to redux store

const mapStateToProps = (state, ownProps) => {
  return {
    config: state.otp.config,
    location: state.otp.currentQuery[ownProps.type],
    currentPosition: state.otp.location.currentPosition,
    sessionSearches: state.otp.location.sessionSearches,
    nearbyStops: state.otp.location.nearbyStops,
    stopsIndex: state.otp.transitIndex.stops
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    addLocationSearch: (location) => { dispatch(addLocationSearch({ location })) },
    setLocation: (type, location) => { dispatch(setLocation({ type, location })) },
    setLocationToCurrent: (type) => { dispatch(setLocationToCurrent({ type })) },
    clearLocation: (type) => { dispatch(clearLocation({ type })) }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(LocationField)
<|endoftext|>


<commit_before>var _ = require('lodash');
var ss = require('simple-statistics');
var evalculist = require('evalculist');
var isItem = require('./item/isItem');
var keyToVarName = require('./keyToVarName');
var urlFinder = require('./urlFinder');

module.exports = function (graph) {
  graph.register('createComputationContextObject', ['isItem','getItemByGuid'], function (isItem, getItemByGuid) {
    var itemMethods = ['$item', '$$item', '$siblings'];

    var fnToString = _.constant('[Function]');

    function ContextObject (item) {
      if (item) {
        var _this = this;
        _.each(itemMethods, function(method) {
          _this[method] = function () {
            return item[method].apply(item, arguments);
          };
          _this[method].toString = fnToString;
        });

        this.$parent = item.parent;
        this.$index = item.parent ? item.parent.items.indexOf(item) : 0;
        this.$items = item.items;
        this.$name = item.key;
        this.$guid = item.guid;
      }
    }

    var proto = ContextObject.prototype;

    var valIfItem = function (item) { return isItem(item) ? item.valueOf() : item; };
    var itemsIfItem = function (item) { return isItem(item) ? item.items : item; };
    var itemsFirst = function (fn) {
      return function () {
        arguments[0] = itemsIfItem(arguments[0]);
        return fn.apply(this, arguments);
      };
    };
    var allItemsAndValues = function (fn) {
      return function () {
        var i = arguments.length;
        while (--i >= 0) {
          arguments[i] = itemsIfItem(arguments[i]);
          if (_.isArray(arguments[i])) arguments[i] = _.map(arguments[i], proto.valueOf);
        }
        return fn.apply(this, arguments);
      };
    };

    var mathKeys = ['E','LN2','LN10','LOG2E','LOG10E','PI','SQRT1_2','SQRT2','abs','acos','acosh','asin','asinh','atan','atan2','atanh','cbrt','ceil','clz32','cos','cosh','exp','expm1','floor','fround','hypot','imul','log','log1p','log2','log10','max','min','pow','random','round','sign','sin','sinh','sqrt','tan','tanh','trunc'];
    var ssKeys = ['median','mode','product','variance','sampleVariance','standardDeviation','sampleStandardDeviation','medianAbsoluteDeviation','interquartileRange','harmonicMean','geometricMean','rootMeanSquare','sampleSkewness','factorial'];
    var lodashKeys = ['difference','intersection','reverse','union','uniq','xor',
                      'filter','find','findLast','map','reduce','zip','unzip',
                      'ceil','floor','max','min','round','clamp','inRange','range',
                      'camelCase','capitalize','kebabCase','lowerCase','join','split',
                      'parseInt','repeat','replace','snakeCase','split','startCase','toLower',
                      'toUpper','truncate','upperCase','words','times','identity'];
    _.each(mathKeys, function(key) { proto[key] = Math[key]; });
    _.each(ssKeys, function(key) { proto[key] = ss[key]; });
    _.each(lodashKeys, function (key) { proto[key] = _[key]; });

    var iterators = ['average','mean','median','mode','standardDeviation','products',
                    'unzip','filter','find','findLast','map','reduce','min','max','join'];

    _.each(iterators, function (methodName) {
      proto[methodName] = itemsFirst(proto[methodName]);
    });

    var doubleIterators = ['difference','intersection','union','xor','zip'];

    _.each(doubleIterators, function (methodName) {
      proto[methodName] = allItemsAndValues(proto[methodName]);
    });

    var conditionals = ['isArray','isNumber','isFunction','isString'];

    _.each(conditionals, function (methodName) {
      proto[methodName] = _.flow(valIfItem, _[methodName]);
    });

    proto.PLACEHOLDER = { toString: _.constant('[PLACEHOLDER]') };

    proto.partial = _.rest(function (fn, partialArgs) {
      var partialFn = _.rest(function (args) {
        var finalArgs = partialArgs.map(function (pArg) {
          if (pArg === proto.PLACEHOLDER) return args.shift();
          return pArg;
        }).concat(args);
        return fn.apply(null, finalArgs);
      });
      partialFn.toString = fnToString;
      return partialFn;
    });

    proto.average = proto.mean = itemsFirst(function (items, defaultValue) {
      return proto.sum(items, defaultValue) / items.length;
    });

    proto.sum = itemsFirst(function(items, defaultValue) {
      if (defaultValue != null) {
        return items.reduce(function(sum, item) {
          var val = item && item.valueOf();
          if (!_.isNumber(val) || _.isNaN(val)) val = defaultValue;
          return sum + val;
        }, 0);
      }
      return items.reduce(function(sum, item) {
        var val = item.valueOf();
        return sum + (_.isNumber(val) ? val : NaN);
      }, 0);
    });

    proto.quantile = itemsFirst(function (sample, p) {
      if (_.isNumber(p)) return ss.quantile(sample, p);
    });

    proto.interquartileRange = itemsFirst(function (items) {
      var values = _.map(items, valIfItem);
      return ss.interquartileRange(values);
    });

    proto.binomialCoefficient = function binomial(n, k) {
      // if ((typeof n !== 'number') || (typeof k !== 'number')) return false;
      var coeff = 1;
      for (var x = n-k+1; x <= n; x++) coeff *= x;
      for (x = 1; x <= k; x++) coeff /= x;
      return coeff;
    };
    proto.isInteger = function (n) { return _.isNumber(n) && n % 1 === 0; }
    proto.gcd = function (a, b) {
      var _gcd = function (a, b) { return b !== 0 ? _gcd(b, a % b) : a; };
      if (proto.isInteger(a) && proto.isInteger(b)) return _gcd(a, b);
      return NaN;
    };
    proto.lcm = function (a, b) { return Math.abs(a) * (Math.abs(b) / proto.gcd(a, b)); };
    proto.fraction = function (numerator, denominator) {
      if (denominator == null) denominator = 1;
      var ndec = numerator.toString().split('.')[1] || '';
      var ddec = denominator.toString().split('.')[1] || '';
      if (ndec || ddec) {
        var adjustor = Math.pow(10, Math.max(ndec.length, ddec.length));
        numerator = Math.round(numerator * adjustor);
        denominator = Math.round(denominator * adjustor);
      }
      var gcd = proto.gcd(numerator, denominator);
      return '' + (numerator / gcd) + '/' +  (denominator / gcd);
    };
    proto.wordCount = function (item) {
      var count = 0;
      var items = item;
      if (isItem(item)) {
        if (_.isUndefined(item.key)) item.valueOf();
        count = _.words('' + item.key + ' ' + (item.val || '')).length;
        items = item.items;
        if (items.length === 0) return count;
      } else if (!_.isArray(items)) {
        return _.words('' + item).length;
      }
      return _.reduce(items, function (sum, item) {
        return sum + proto.wordCount(item);
      }, count);
    };
    proto.mod = proto.modulo = function(a, b) { return (+a % (b = +b) + b) % b; };
    proto.polarToCartesian = function (r, theta) {
      var x = r * Math.cos(theta),
          y = r * Math.sin(theta);
      return [x, y];
    };
    proto.cartesianToPolar = function (x, y) {
      var r = Math.sqrt(x * x + y * y),
          theta = Math.atan(y / x);
      return [r, theta];
    };
    proto.degreesToRadians = function (degrees) {
      return (degrees / 360) * (2 * Math.PI);
    };

    proto.radiansToDegrees = function (radians) {
      return 360 * radians / (2 * Math.PI);
    };

    proto.length = proto.count = itemsFirst(_.property('length'));
    proto.name = proto.key = _.property('key');
    proto.valueOf = _.method('valueOf');
    proto.toString = _.method('toString');
    proto.recursiveCount = proto.rcount = itemsFirst(function (items) {
      return items.length + _.reduce(items, function (m, item) {
        return m + proto.recursiveCount(item.items);
      }, 0);
    });

    proto.true = true;
    proto.false = false;
    proto.null = null;

    proto.itemOf = function (item, name) {
      return item.$item(name);
    };
    proto.itemsOf = _.property('items');
    proto.nameOf = _.property('key');
    proto.parentOf = _.property('parent');
    proto.guidOf = _.property('guid');
    proto.indexOf = itemsFirst(function (array, item) {
      return array.indexOf(item);
    });
    proto.pluckItems = itemsFirst(function (items, key) {
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return items.reduce(function (pluckedItems, item) {
        var pluckedItem = _.find(item.items, condition);
        if (pluckedItem) pluckedItems.push(pluckedItem);
        return pluckedItems;
      }, []);
    });

    proto.function = proto.fn = _.rest(function (string, partialArgs) {
      var pieces = ('' + string).split('|');
      var argNames = pieces.length > 1 ? pieces.shift().split(',') : [];
      var fnBody = pieces.join('|')
      var evalFn = evalculist(fnBody);
      var fn = function () {
        var args = partialArgs.concat(_.toArray(arguments));
        var localVars = argNames.reduce(function (localVars, name, i) {
          localVars[_.trim(name)] = args[i];
          return localVars;
        }, {});
        return evalFn({
          variable: function (name) {
            if (localVars.hasOwnProperty(name)) return localVars[name];
            return proto[name];
          },
          accessor: proto.accessor,
          dotAccessor: proto.dotAccessor,
          assignment: function (name, val) {
            return localVars[name] = val;
          }
        });
      };
      var argString = argNames.slice(partialArgs.length).join(',');
      var fnString = argString ? (argString + '|'  + fnBody) : fnBody;
      fn.toString = _.constant(fnString);
      return fn;
    });

    proto.flowMap = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var fmap = _.rest(function (args) {
        return _.map(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      });
      fmap.toString = fnToString;
      return fmap;
    });

    proto.flow = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var flow = function () {
        return _.reduce(fns, function (r, fn) {
          return [fn.valueOf().apply(null, r)];
        }, arguments)[0];
      };
      flow.toString = fnToString;
      return flow;
    });

    proto.compose = itemsFirst(function () {
      var fns = _.flatten(arguments);
      fns.reverse();
      return proto.flow(fns);
    });

    var curry2 = function (fn) {
      var fn0, fn1;
      fn0 = function () {
        if (arguments.length === 0) return fn0;
        var arg0 = arguments[0];
        fn1 = function (arg1) { return fn(arg0, arg1); };
        if (arguments.length > 1) return fn1(arguments[1]);
        fn1.toString = fnToString;
        return fn1;
      };
      fn0.toString = fnToString;
      return fn0;
    };

    proto.isEqualTo = proto.eq = curry2(function (b, a) { return a == b; });
    proto.isGreaterThan = proto.gt = curry2(function (b, a) { return a > b; });
    proto.isGreaterThanOrEqualTo = proto.gte = curry2(function (b, a) { return a >= b; });
    proto.isLessThan = proto.lt = curry2(function (b, a) { return a < b; });
    proto.isLessThanOrEqualTo = proto.lte = curry2(function (b, a) { return a <= b; });

    // TODO should these logic functions check and adapt to non-function arguments?
    proto.and = function () {
      var fns = _.flatten(arguments),
          and;
      and = function () {
        var args = arguments;
        return _.every(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      and.toString = fnToString;
      return and;
    };

    proto.or = function () {
      var fns = _.flatten(arguments),
          or;
      or = function () {
        var args = arguments;
        return _.some(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      or.toString = fnToString;
      return or;
    };

    proto.not = function (fn) {
      return function () {
        return !fn.valueOf().apply(null, arguments);
      };
    };

    proto.item = curry2(function (key, list) {
      if (isItem(list)) return list.$item(key);
      if (_.isNumber(key)) return list[key];
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return _.find(list, condition);
    });

    proto.global_item = function (key) {
      return window.topItem.$item(key);
    };

    proto.itemByGuid = function (guid) {
      return getItemByGuid(guid);
    };

    // evalculist adds a special "accessor" function for things like a['b']
    // so it becomes accessor(a, 'b')
    proto.accessor = function (obj, key) {
      if (_.isNumber(key) && !isItem(obj[key])) return obj[key];
      return proto.pluckItems(obj, key);
    };

    proto.dotAccessor = itemsFirst(function (items, key) {
      var item = _.findLast(items, function (item) {
        if (!item.key) item.valueOf();
        return keyToVarName(item.key) === key;
      });
      if (item && item.hasVal) return item.valueOf();
      return item;
    });

    proto.flatten = itemsFirst(function (items) {
      if (isItem(items[0])) {
        return _.reduce(items, function (flatItems, item) {
          flatItems.push(item);
          if (item.items.length) return flatItems.concat(proto.flatten(item));
          return flatItems;
        }, []);
      } else {
        return _.flatten(items);
      }
    });

    var imageToString = _.constant('[Image]');
    proto.image = function (url, width, height) {
      if (!urlFinder.isUrl(url)) return NaN;
      var html = '<img src="' + url + '"';
      if (_.isNumber(+width)) html += ' width="' + (+width) + '"';
      if (_.isNumber(+height)) html += ' height="' + (+height) + '"';
      html += '/>';
      return {
        mediaType: 'image',
        toString: imageToString,
        toHTML: _.constant(html),
      };
    };

    proto.uniq = proto.unique = itemsFirst(function (items) {
      items = _.map(items, proto.valueOf);
      return _.uniq(items);
    });

    proto.slice = itemsFirst(function (items, start, end) {
      var result = _.slice(items, start, end);
      if (_.isString(items)) return result.join('');
      return result;
    });

    proto.reverse = itemsFirst(function (items) {
      var result = _.reverse(_.toArray(items));
      if (_.isString(items)) return result.join('');
      return result;
    });

    _.each(proto, function (fn, key) {
      proto[_.snakeCase(key)] = fn;
      if (_.isFunction(fn)) fn.toString = fnToString;
    });

    return function (item) {
      return new ContextObject(item);
    };

  });
};
<commit_msg>Update "itemOf" to match "item"<commit_after>var _ = require('lodash');
var ss = require('simple-statistics');
var evalculist = require('evalculist');
var isItem = require('./item/isItem');
var keyToVarName = require('./keyToVarName');
var urlFinder = require('./urlFinder');

module.exports = function (graph) {
  graph.register('createComputationContextObject', ['isItem','getItemByGuid'], function (isItem, getItemByGuid) {
    var itemMethods = ['$item', '$$item', '$siblings'];

    var fnToString = _.constant('[Function]');

    function ContextObject (item) {
      if (item) {
        var _this = this;
        _.each(itemMethods, function(method) {
          _this[method] = function () {
            return item[method].apply(item, arguments);
          };
          _this[method].toString = fnToString;
        });

        this.$parent = item.parent;
        this.$index = item.parent ? item.parent.items.indexOf(item) : 0;
        this.$items = item.items;
        this.$name = item.key;
        this.$guid = item.guid;
      }
    }

    var proto = ContextObject.prototype;

    var valIfItem = function (item) { return isItem(item) ? item.valueOf() : item; };
    var itemsIfItem = function (item) { return isItem(item) ? item.items : item; };
    var itemsFirst = function (fn) {
      return function () {
        arguments[0] = itemsIfItem(arguments[0]);
        return fn.apply(this, arguments);
      };
    };
    var allItemsAndValues = function (fn) {
      return function () {
        var i = arguments.length;
        while (--i >= 0) {
          arguments[i] = itemsIfItem(arguments[i]);
          if (_.isArray(arguments[i])) arguments[i] = _.map(arguments[i], proto.valueOf);
        }
        return fn.apply(this, arguments);
      };
    };

    var mathKeys = ['E','LN2','LN10','LOG2E','LOG10E','PI','SQRT1_2','SQRT2','abs','acos','acosh','asin','asinh','atan','atan2','atanh','cbrt','ceil','clz32','cos','cosh','exp','expm1','floor','fround','hypot','imul','log','log1p','log2','log10','max','min','pow','random','round','sign','sin','sinh','sqrt','tan','tanh','trunc'];
    var ssKeys = ['median','mode','product','variance','sampleVariance','standardDeviation','sampleStandardDeviation','medianAbsoluteDeviation','interquartileRange','harmonicMean','geometricMean','rootMeanSquare','sampleSkewness','factorial'];
    var lodashKeys = ['difference','intersection','reverse','union','uniq','xor',
                      'filter','find','findLast','map','reduce','zip','unzip',
                      'ceil','floor','max','min','round','clamp','inRange','range',
                      'camelCase','capitalize','kebabCase','lowerCase','join','split',
                      'parseInt','repeat','replace','snakeCase','split','startCase','toLower',
                      'toUpper','truncate','upperCase','words','times','identity'];
    _.each(mathKeys, function(key) { proto[key] = Math[key]; });
    _.each(ssKeys, function(key) { proto[key] = ss[key]; });
    _.each(lodashKeys, function (key) { proto[key] = _[key]; });

    var iterators = ['average','mean','median','mode','standardDeviation','products',
                    'unzip','filter','find','findLast','map','reduce','min','max','join'];

    _.each(iterators, function (methodName) {
      proto[methodName] = itemsFirst(proto[methodName]);
    });

    var doubleIterators = ['difference','intersection','union','xor','zip'];

    _.each(doubleIterators, function (methodName) {
      proto[methodName] = allItemsAndValues(proto[methodName]);
    });

    var conditionals = ['isArray','isNumber','isFunction','isString'];

    _.each(conditionals, function (methodName) {
      proto[methodName] = _.flow(valIfItem, _[methodName]);
    });

    proto.PLACEHOLDER = { toString: _.constant('[PLACEHOLDER]') };

    proto.partial = _.rest(function (fn, partialArgs) {
      var partialFn = _.rest(function (args) {
        var finalArgs = partialArgs.map(function (pArg) {
          if (pArg === proto.PLACEHOLDER) return args.shift();
          return pArg;
        }).concat(args);
        return fn.apply(null, finalArgs);
      });
      partialFn.toString = fnToString;
      return partialFn;
    });

    proto.average = proto.mean = itemsFirst(function (items, defaultValue) {
      return proto.sum(items, defaultValue) / items.length;
    });

    proto.sum = itemsFirst(function(items, defaultValue) {
      if (defaultValue != null) {
        return items.reduce(function(sum, item) {
          var val = item && item.valueOf();
          if (!_.isNumber(val) || _.isNaN(val)) val = defaultValue;
          return sum + val;
        }, 0);
      }
      return items.reduce(function(sum, item) {
        var val = item.valueOf();
        return sum + (_.isNumber(val) ? val : NaN);
      }, 0);
    });

    proto.quantile = itemsFirst(function (sample, p) {
      if (_.isNumber(p)) return ss.quantile(sample, p);
    });

    proto.interquartileRange = itemsFirst(function (items) {
      var values = _.map(items, valIfItem);
      return ss.interquartileRange(values);
    });

    proto.binomialCoefficient = function binomial(n, k) {
      // if ((typeof n !== 'number') || (typeof k !== 'number')) return false;
      var coeff = 1;
      for (var x = n-k+1; x <= n; x++) coeff *= x;
      for (x = 1; x <= k; x++) coeff /= x;
      return coeff;
    };
    proto.isInteger = function (n) { return _.isNumber(n) && n % 1 === 0; }
    proto.gcd = function (a, b) {
      var _gcd = function (a, b) { return b !== 0 ? _gcd(b, a % b) : a; };
      if (proto.isInteger(a) && proto.isInteger(b)) return _gcd(a, b);
      return NaN;
    };
    proto.lcm = function (a, b) { return Math.abs(a) * (Math.abs(b) / proto.gcd(a, b)); };
    proto.fraction = function (numerator, denominator) {
      if (denominator == null) denominator = 1;
      var ndec = numerator.toString().split('.')[1] || '';
      var ddec = denominator.toString().split('.')[1] || '';
      if (ndec || ddec) {
        var adjustor = Math.pow(10, Math.max(ndec.length, ddec.length));
        numerator = Math.round(numerator * adjustor);
        denominator = Math.round(denominator * adjustor);
      }
      var gcd = proto.gcd(numerator, denominator);
      return '' + (numerator / gcd) + '/' +  (denominator / gcd);
    };
    proto.wordCount = function (item) {
      var count = 0;
      var items = item;
      if (isItem(item)) {
        if (_.isUndefined(item.key)) item.valueOf();
        count = _.words('' + item.key + ' ' + (item.val || '')).length;
        items = item.items;
        if (items.length === 0) return count;
      } else if (!_.isArray(items)) {
        return _.words('' + item).length;
      }
      return _.reduce(items, function (sum, item) {
        return sum + proto.wordCount(item);
      }, count);
    };
    proto.mod = proto.modulo = function(a, b) { return (+a % (b = +b) + b) % b; };
    proto.polarToCartesian = function (r, theta) {
      var x = r * Math.cos(theta),
          y = r * Math.sin(theta);
      return [x, y];
    };
    proto.cartesianToPolar = function (x, y) {
      var r = Math.sqrt(x * x + y * y),
          theta = Math.atan(y / x);
      return [r, theta];
    };
    proto.degreesToRadians = function (degrees) {
      return (degrees / 360) * (2 * Math.PI);
    };

    proto.radiansToDegrees = function (radians) {
      return 360 * radians / (2 * Math.PI);
    };

    proto.length = proto.count = itemsFirst(_.property('length'));
    proto.name = proto.key = _.property('key');
    proto.valueOf = _.method('valueOf');
    proto.toString = _.method('toString');
    proto.recursiveCount = proto.rcount = itemsFirst(function (items) {
      return items.length + _.reduce(items, function (m, item) {
        return m + proto.recursiveCount(item.items);
      }, 0);
    });

    proto.true = true;
    proto.false = false;
    proto.null = null;

    proto.itemsOf = _.property('items');
    proto.nameOf = _.property('key');
    proto.parentOf = _.property('parent');
    proto.guidOf = _.property('guid');
    proto.indexOf = itemsFirst(function (array, item) {
      return array.indexOf(item);
    });
    proto.pluckItems = itemsFirst(function (items, key) {
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return items.reduce(function (pluckedItems, item) {
        var pluckedItem = _.find(item.items, condition);
        if (pluckedItem) pluckedItems.push(pluckedItem);
        return pluckedItems;
      }, []);
    });

    proto.function = proto.fn = _.rest(function (string, partialArgs) {
      var pieces = ('' + string).split('|');
      var argNames = pieces.length > 1 ? pieces.shift().split(',') : [];
      var fnBody = pieces.join('|')
      var evalFn = evalculist(fnBody);
      var fn = function () {
        var args = partialArgs.concat(_.toArray(arguments));
        var localVars = argNames.reduce(function (localVars, name, i) {
          localVars[_.trim(name)] = args[i];
          return localVars;
        }, {});
        return evalFn({
          variable: function (name) {
            if (localVars.hasOwnProperty(name)) return localVars[name];
            return proto[name];
          },
          accessor: proto.accessor,
          dotAccessor: proto.dotAccessor,
          assignment: function (name, val) {
            return localVars[name] = val;
          }
        });
      };
      var argString = argNames.slice(partialArgs.length).join(',');
      var fnString = argString ? (argString + '|'  + fnBody) : fnBody;
      fn.toString = _.constant(fnString);
      return fn;
    });

    proto.flowMap = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var fmap = _.rest(function (args) {
        return _.map(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      });
      fmap.toString = fnToString;
      return fmap;
    });

    proto.flow = itemsFirst(function () {
      var fns = _.flatten(arguments);
      var flow = function () {
        return _.reduce(fns, function (r, fn) {
          return [fn.valueOf().apply(null, r)];
        }, arguments)[0];
      };
      flow.toString = fnToString;
      return flow;
    });

    proto.compose = itemsFirst(function () {
      var fns = _.flatten(arguments);
      fns.reverse();
      return proto.flow(fns);
    });

    var curry2 = function (fn) {
      var fn0, fn1;
      fn0 = function () {
        if (arguments.length === 0) return fn0;
        var arg0 = arguments[0];
        fn1 = function (arg1) { return fn(arg0, arg1); };
        if (arguments.length > 1) return fn1(arguments[1]);
        fn1.toString = fnToString;
        return fn1;
      };
      fn0.toString = fnToString;
      return fn0;
    };

    proto.isEqualTo = proto.eq = curry2(function (b, a) { return a == b; });
    proto.isGreaterThan = proto.gt = curry2(function (b, a) { return a > b; });
    proto.isGreaterThanOrEqualTo = proto.gte = curry2(function (b, a) { return a >= b; });
    proto.isLessThan = proto.lt = curry2(function (b, a) { return a < b; });
    proto.isLessThanOrEqualTo = proto.lte = curry2(function (b, a) { return a <= b; });

    // TODO should these logic functions check and adapt to non-function arguments?
    proto.and = function () {
      var fns = _.flatten(arguments),
          and;
      and = function () {
        var args = arguments;
        return _.every(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      and.toString = fnToString;
      return and;
    };

    proto.or = function () {
      var fns = _.flatten(arguments),
          or;
      or = function () {
        var args = arguments;
        return _.some(fns, function (fn) {
          return fn.valueOf().apply(null, args);
        });
      };
      or.toString = fnToString;
      return or;
    };

    proto.not = function (fn) {
      return function () {
        return !fn.valueOf().apply(null, arguments);
      };
    };

    proto.item = curry2(function (key, list) {
      if (isItem(list)) return list.$item(key);
      if (_.isNumber(key)) return list[key];
      var condition;
      if (_.isFunction(key)) {
        condition = key;
      } else {
        condition = { key: key };
      }
      return _.find(list, condition);
    });

    proto.itemOf = curry2(function (list, key) {
      return proto.item(key, list);
    });

    proto.global_item = function (key) {
      return window.topItem.$item(key);
    };

    proto.itemByGuid = function (guid) {
      return getItemByGuid(guid);
    };

    // evalculist adds a special "accessor" function for things like a['b']
    // so it becomes accessor(a, 'b')
    proto.accessor = function (obj, key) {
      if (_.isNumber(key) && !isItem(obj[key])) return obj[key];
      return proto.pluckItems(obj, key);
    };

    proto.dotAccessor = itemsFirst(function (items, key) {
      var item = _.findLast(items, function (item) {
        if (!item.key) item.valueOf();
        return keyToVarName(item.key) === key;
      });
      if (item && item.hasVal) return item.valueOf();
      return item;
    });

    proto.flatten = itemsFirst(function (items) {
      if (isItem(items[0])) {
        return _.reduce(items, function (flatItems, item) {
          flatItems.push(item);
          if (item.items.length) return flatItems.concat(proto.flatten(item));
          return flatItems;
        }, []);
      } else {
        return _.flatten(items);
      }
    });

    var imageToString = _.constant('[Image]');
    proto.image = function (url, width, height) {
      if (!urlFinder.isUrl(url)) return NaN;
      var html = '<img src="' + url + '"';
      if (_.isNumber(+width)) html += ' width="' + (+width) + '"';
      if (_.isNumber(+height)) html += ' height="' + (+height) + '"';
      html += '/>';
      return {
        mediaType: 'image',
        toString: imageToString,
        toHTML: _.constant(html),
      };
    };

    proto.uniq = proto.unique = itemsFirst(function (items) {
      items = _.map(items, proto.valueOf);
      return _.uniq(items);
    });

    proto.slice = itemsFirst(function (items, start, end) {
      var result = _.slice(items, start, end);
      if (_.isString(items)) return result.join('');
      return result;
    });

    proto.reverse = itemsFirst(function (items) {
      var result = _.reverse(_.toArray(items));
      if (_.isString(items)) return result.join('');
      return result;
    });

    _.each(proto, function (fn, key) {
      proto[_.snakeCase(key)] = fn;
      if (_.isFunction(fn)) fn.toString = fnToString;
    });

    return function (item) {
      return new ContextObject(item);
    };

  });
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import HelpIcon from "@material-ui/icons/Help";

import {
    Button,
    Card,
    Checkbox,
    FormControlLabel,
    Input,
    Menu,
    MenuItem,
    Select,
    Tooltip,
    Typography
} from '@material-ui/core';

import { mergeTemplateInfoShape } from './MergeTemplatePropTypes';

export default class MergeTemplateInputForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
            textInputValue: this.props.textInputValue,
            formInputValue: this.props.formInputValue,
            menuInputSelected: this.props.menuInputSelected,
            // mergeTemplateInfo: this.props.mergeTemplateInfo,
            sheetTabs: ["TODO: ADD TABS", 1, 2, 3],
            sheetHeaders: "TODO: ADD HEADERS", // ?? should this be in this component or handled by other component??
            // selectedTab: "Hello!"
            // anchorElement: null
        } // Add selected tab to state?? -> this.state = ...this.state, this.props.selectedTab

        this.handleTextInput = this.handleTextInput.bind(this);
        this.handleFormInput = this.handleFormInput.bind(this);

        console.log("MergeTemplateInputForm State: ", this.state);

        // if (this.props.textInputCallback) {
        //     this.props.textInputCallback(this.state.mergeTemplateInfo.title); // Initialize value in parent - YOU SHOULD NOT HAVE TO DO THIS, INITIALIZE FROM PARENT!!!
        // }
        // if (this.props.formControlCallback) {
        //     this.props.formControlCallback(this.state.mergeTemplateInfo.timestampColumn.shouldPrefixNameWithMergeTemplateTitle);
        // }
    }

    render() {
        return (
            <Card style={styles.container}>
                <Typography variant="h5" style={styles.title}>{this.props.title}</Typography>
                {this.renderTextInput()}
                {this.renderMenuInput()}
                {this.renderFormInput()}
                {this.renderTip()}
            </Card>
        );
    }

    handleTextInput(event) {
        this.setState({
            textInputValue: event.target.value
        });
        if (this.props.textInputCallback) {
            this.props.textInputCallback(event.target.value);
        }
    }

    renderTextInput() {
        if (this.props.textInputTitle) {
            return (
                <Input
                    name="text_input"
                    placeholder={this.props.textInputTitle}
                    onChange={this.handleTextInput}
                    value={this.state.textInputValue}
                    style={styles.textInput}
                />
            );
        } else {
            return null;
        }
    }

    handleFormInput() {
        var currentValue = this.state.formInputValue;
        this.setState({
            formInputValue: !currentValue
        });
        if (this.props.formInputCallback) {
            this.props.formInputCallback(!currentValue);
        }
    }

    renderFormInput() {
        if (this.props.formInputTitle) {
            return (
                <FormControlLabel
                    name="form_input"
                    control={
                        <Checkbox
                            color="primary"
                            style={styles.formInputCheckbox}
                            checked={this.state.formInputValue}
                            onChange={this.handleFormInput}
                        />
                    }
                    label={
                        <Typography
                            variant="caption"
                        >
                            {this.props.formInputTitle}
                        </Typography>
                    }
                    labelPlacement="end"
                    style={styles.formInput}
                />
            );
        } else {
            return null;
        }
    }

    // TODO: Drop-down menu -> get input from props

    handleClick = (event) => {
        this.setState({ anchorElement: event.currentTarget });
    }

    handleClose = () => {
        this.setState({ anchorElement: null });
    }

    createMenuItems() {
        return (
            // <React.Fragment>
            //     { this.state.sheetTabs.map(function(tab) {
            //         return (
            //             <MenuItem value={tab}>{tab}</MenuItem>
            //         )
            //     }) }
            // </React.Fragment>
            this.props.menuInputValues.map( function(tab) {
                return (
                    <MenuItem value={tab} key={tab}>{tab}</MenuItem>
                )
            })
        );
    }

    renderMenuInput() {
        if (this.props.menuInput) {

            // const { anchorElement } = this.state;

            return (
                <Select
                    style={styles.menuInput}
                    value={this.state.menuInputSelected}
                >
                    {this.createMenuItems()}
                </Select>
                // <React.Fragment>
                //     <Button
                //         onClick={this.handleClick}
                //     >
                //         Open Menu
                //     </Button>
                //     <Menu
                //         anchorEl={anchorElement}
                //         open={Boolean(anchorElement)}
                //         onClick={() => console.log("Menu was clicked. TODO: get the available tabs?? - use a callback function??")}
                //         onClose={this.handleClose}
                //         style={styles.menuInput}
                //     >
                //         <Typography onClick={this.handleClose} style={styles.menuItem}>Menu Item 1</Typography>
                //         <Typography onClick={this.handleClose} style={styles.menuItem}>Menu Item 2</Typography>
                //         <Typography onClick={this.handleClose} style={styles.menuItem}>Menu Item 3</Typography>
                //     </Menu>
                // </React.Fragment>
            );
        } else {
            return null;
        }
    }

    renderTip() {
        if (this.props.tip) {
            return (
                <Tooltip title={this.props.tip} style={styles.tip}><HelpIcon/></Tooltip>
            );
        } else {
            return null;
        }
    }
}

const styles = {
    container: {
        flex: 1,
        display: 'flex',
        flexDirection: 'column',
        padding: 15,
        justifyContent: 'center',
    },
    title: {
        marginBottom: 15
    },
    textInput: {
        marginTop: 15
    },
    formInput: {
        marginTop: 15
    },
    formInputCheckbox: {
        position: "relative",
        top: 0
    },
    menuInput: {
        marginTop: 15
    },
    tip: {
        marginTop: 15,
    }
}

MergeTemplateInputForm.propTypes = {
    title: PropTypes.string.isRequired,
    tip: PropTypes.string,
    checkbox: PropTypes.string,
    textInputTitle: PropTypes.string,
    textInputValue: function(props, propName, componentName) {
        if (props['textInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'string')) {
            return new Error(
                "Please provide a textInputValue string!"
            );
        }
    },
    textInputCallback: function(props, propName, componentName) {
        if (props['textInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'function')) {
            return new Error(
                "Please provide a textInputCallback function!"
            );
        }
    },
    formInputTitle: PropTypes.string,
    formInputValue: function(props, propName, componentName) {
        if (props['formInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'boolean')) {
            return new Error(
                "Please provide a formInputValue boolean!"
            );
        }
    },
    formInputCallback: function(props, propName, componentName) {
        if (props['formInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'function')) {
            return new Error(
                "Please provide a formInputCallback function!"
            );
        }
    },
    menuInput: PropTypes.bool,
    menuInputSelected: PropTypes.string, // The initial value of the menu
    menuInputValues: PropTypes.array,
    // menuInputCallback: function(props, propName, componentName) {
    //     if ((props['menuInput'] && (props[propName] === undefined || typeof(props[propName]) !== 'function'))) {
    //         return new Error(
    //             "Please provide a menuInputCallback function!"
    //         );
    //     }
    // },
    sheetTabs: PropTypes.array,
    sheetHeaders: PropTypes.array
}<commit_msg>Working on Selection menu input<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import HelpIcon from "@material-ui/icons/Help";

import {
    Button,
    Card,
    Checkbox,
    FormControlLabel,
    Input,
    Menu,
    MenuItem,
    Select,
    Tooltip,
    Typography
} from '@material-ui/core';

import { mergeTemplateInfoShape } from './MergeTemplatePropTypes';

export default class MergeTemplateInputForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
            textInputValue: this.props.textInputValue,
            formInputValue: this.props.formInputValue,
            menuInputSelected: this.props.menuInputSelected,
            // mergeTemplateInfo: this.props.mergeTemplateInfo,
            sheetTabs: ["TODO: ADD TABS", 1, 2, 3],
            sheetHeaders: "TODO: ADD HEADERS", // ?? should this be in this component or handled by other component??
            // selectedTab: "Hello!"
            // anchorElement: null
        } // Add selected tab to state?? -> this.state = ...this.state, this.props.selectedTab

        this.handleTextInput = this.handleTextInput.bind(this);
        this.handleFormInput = this.handleFormInput.bind(this);

        console.log("MergeTemplateInputForm State: ", this.state);

        // if (this.props.textInputCallback) {
        //     this.props.textInputCallback(this.state.mergeTemplateInfo.title); // Initialize value in parent - YOU SHOULD NOT HAVE TO DO THIS, INITIALIZE FROM PARENT!!!
        // }
        // if (this.props.formControlCallback) {
        //     this.props.formControlCallback(this.state.mergeTemplateInfo.timestampColumn.shouldPrefixNameWithMergeTemplateTitle);
        // }
    }

    render() {
        return (
            <Card style={styles.container}>
                <Typography variant="h5" style={styles.title}>{this.props.title}</Typography>
                {this.renderTextInput()}
                {this.renderMenuInput()}
                {this.renderFormInput()}
                {this.renderTip()}
            </Card>
        );
    }

    handleTextInput(event) {
        this.setState({
            textInputValue: event.target.value
        });
        if (this.props.textInputCallback) {
            this.props.textInputCallback(event.target.value);
        }
    }

    renderTextInput() {
        if (this.props.textInputTitle) {
            return (
                <Input
                    name="text_input"
                    placeholder={this.props.textInputTitle}
                    onChange={this.handleTextInput}
                    value={this.state.textInputValue}
                    style={styles.textInput}
                />
            );
        } else {
            return null;
        }
    }

    handleFormInput() {
        var currentValue = this.state.formInputValue;
        this.setState({
            formInputValue: !currentValue
        });
        if (this.props.formInputCallback) {
            this.props.formInputCallback(!currentValue);
        }
    }

    renderFormInput() {
        if (this.props.formInputTitle) {
            return (
                <FormControlLabel
                    name="form_input"
                    control={
                        <Checkbox
                            color="primary"
                            style={styles.formInputCheckbox}
                            checked={this.state.formInputValue}
                            onChange={this.handleFormInput}
                        />
                    }
                    label={
                        <Typography
                            variant="caption"
                        >
                            {this.props.formInputTitle}
                        </Typography>
                    }
                    labelPlacement="end"
                    style={styles.formInput}
                />
            );
        } else {
            return null;
        }
    }

    // TODO: Drop-down menu -> get input from props

    // TODO: Handle callback! -> update selected from parent view

    createMenuItems() {
        return (
            this.props.menuInputValues.map( function(tab) {
                return (
                    <MenuItem value={tab} key={tab}>{tab}</MenuItem>
                )
            })
        );
    }

    renderMenuInput() {
        if (this.props.menuInput) {
            return (
                <Select
                    style={styles.menuInput}
                    value={this.state.menuInputSelected}
                    // TODO: Handle change and selection!!!
                >
                    {this.createMenuItems()}
                </Select>
            );
        } else {
            return null;
        }
    }

    renderTip() {
        if (this.props.tip) {
            return (
                <Tooltip title={this.props.tip} style={styles.tip}><HelpIcon/></Tooltip>
            );
        } else {
            return null;
        }
    }
}

const styles = {
    container: {
        flex: 1,
        display: 'flex',
        flexDirection: 'column',
        padding: 15,
        justifyContent: 'center',
    },
    title: {
        marginBottom: 15
    },
    textInput: {
        marginTop: 15
    },
    formInput: {
        marginTop: 15
    },
    formInputCheckbox: {
        position: "relative",
        top: 0
    },
    menuInput: {
        marginTop: 15
    },
    tip: {
        marginTop: 15,
    }
}

MergeTemplateInputForm.propTypes = {
    title: PropTypes.string.isRequired,
    tip: PropTypes.string,
    checkbox: PropTypes.string,
    textInputTitle: PropTypes.string,
    textInputValue: function(props, propName, componentName) {
        if (props['textInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'string')) {
            return new Error(
                "Please provide a textInputValue string!"
            );
        }
    },
    textInputCallback: function(props, propName, componentName) {
        if (props['textInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'function')) {
            return new Error(
                "Please provide a textInputCallback function!"
            );
        }
    },
    formInputTitle: PropTypes.string,
    formInputValue: function(props, propName, componentName) {
        if (props['formInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'boolean')) {
            return new Error(
                "Please provide a formInputValue boolean!"
            );
        }
    },
    formInputCallback: function(props, propName, componentName) {
        if (props['formInputTitle'] && (props[propName] === undefined || typeof(props[propName]) !== 'function')) {
            return new Error(
                "Please provide a formInputCallback function!"
            );
        }
    },
    menuInput: PropTypes.bool,
    menuInputSelected: PropTypes.string, // The initial value of the menu
    menuInputValues: PropTypes.arrayOf(PropTypes.string),
    // menuInputCallback: function(props, propName, componentName) {
    //     if ((props['menuInput'] && (props[propName] === undefined || typeof(props[propName]) !== 'function'))) {
    //         return new Error(
    //             "Please provide a menuInputCallback function!"
    //         );
    //     }
    // },
    sheetTabs: PropTypes.array,
    sheetHeaders: PropTypes.array
}<|endoftext|>