hilight.js

'use strict';

// TODO hilight layer would be a good candidate for a custom class which internally proxies to ESRI's GraphicsLayer.

const defaultSymbols = require('./defaultHilightSymbols.json');

// contains functions to support the hilight layer.

function cloneBuilder(esriBundle) {
    /**
    * Clone a graphic from a map-bound layer.
    * @method cloneLayerGraphic
    * @param {Graphic} graphic an ESRI graphic that resides in a map layer.
    * @return {Object} an unbound copy of the graphic
    */
    return graphic => {
        const clone = new esriBundle.Graphic({
                geometry: graphic.geometry
            });
        clone.symbol = graphic.getLayer().renderer.getSymbol(graphic);
        return clone;
    };
}

function graphicBuilder(esriBundle) {
    /**
    * Generating a graphic from server geometry.
    * @method geomToGraphic
    * @param {Object} geometry feature geometry conforming to ESRI Geometry standard
    * @param {Object} symbol esri symbol in server format
    * @return {Object} an ESRI GraphicsLayer
    */
    return (geometry, symbol) => {
        const graphic = new esriBundle.Graphic({
                geometry
            });
        graphic.symbol = esriBundle.symbolJsonUtils.fromJson(symbol);
        return graphic;
    };
}

function getGraphicsBuilder(esriBundle, geoApi) {
    // TODO once document sites are up and running, figure out a way to hyperlink the graphicBundles parameter to the class documentation page in the viewer site
    /**
    * Generating a graphic from server geometry.
    * @method getUnboundGraphics
    * @param {Array} graphicBundles set of graphic bundles with properties .graphic, .source, .layer, .featureIdx.
    * @param {Object} spatialReference the projection the unbound graphics should be in
    * @return {Array} a set of promises that resolve with an unbound graphic, one for each graphic bundle provided
    */
    return (graphicBundles, spatialReference) => {

        // generate detached graphics to give to the hilight layer.
        // promises because server layers renderer is inside a promise
        return graphicBundles.map(bundle => {
            if (bundle.source === 'server') {
                let geom = bundle.graphic.geometry;

                // check projection
                if (!geoApi.proj.isSpatialRefEqual(geom.spatialReference, spatialReference)) {
                    geom = geoApi.proj.localProjectGeometry(spatialReference, geom);
                }

                // determine symbol for this server graphic
                const attribs = bundle.layer.attributeBundle;
                return attribs[bundle.featureIdx].layerData.then(layerData => {
                    const symb = geoApi.symbology.getGraphicSymbol(bundle.graphic.attributes, layerData.renderer);
                    return geoApi.hilight.geomToGraphic(geom, symb);
                });

            } else {
                // local graphic. clone and hilight
                return Promise.resolve(geoApi.hilight.cloneLayerGraphic(bundle.graphic));
            }
        });
    };
}

function hilightBuilder(esriBundle) {
    /**
    * Generate a graphic layer to handle feature hilighting.
    * @method makeHilightLayer
    * @param {Object} options optional settings for the hilight layer
    *                         layerId - id to use for the hilight layer. defaults to rv_hilight
    *                         pinSymbol - esri symbol in server json format to symbolize the click marker. defaults to a red pin
    *                         hazeOpacity -  how opaque the haze sheet behind the hilight is. 0 to 255, 0 being transparent. defaults to 127
    * @return {Object} an ESRI GraphicsLayer
    */
    return options => {
        // set options
        let id = 'rv_hilight';
        let hazeOpac = 127;
        let pinSymbol = defaultSymbols.pinSymbol;

        if (options) {
            if (options.layerId) {
                id = options.layerId;
            }
            if (options.pinSymbol) {
                pinSymbol = options.pinSymbol;
            }
            if (options.hazeOpacity) {
                hazeOpac = options.hazeOpacity;
            }
        }

        const hgl = new esriBundle.GraphicsLayer({ id, visible: true });

        // ensure highlight is top-most graphic layer
        function moveHilightToTop() {
            hgl._map.reorderLayer(hgl, hgl._map.graphicsLayerIds.length);
        }

        /**
        * Add a graphic to indicate where user clicked.
        * @method addPin
        * @param {Point} point an ESRI point object to use as the graphic location
        */
        hgl.addPin = point => {

            const pin = new esriBundle.Graphic({ symbol: pinSymbol });
            pin.setGeometry(point);

            hgl.add(pin);
            moveHilightToTop();
        };

        /**
        * Add a graphic or array of graphics to the highlight layer. Remove any previous graphics.
        * @method addHilight
        * @param {Graphic|Array} graphic an ESRI graphic, or array of ESRI graphics. Should be in map spatialReference, and not bound to a layer
        */
        hgl.addHilight = graphic => {

            const graphics = Array.isArray(graphic) ? graphic : [graphic];

            if (hgl._hilightGraphics) {
                // if active hilight graphics, remove them
                hgl._hilightGraphics.forEach(g => hgl.remove(g));
            } else {
                // first application of hilight. add haze background by creating a partially opaque layer for
                // the whole map extent with some buffer. This will go under the highlighted graphic to make it stand out.
                const hazeJson = {
                    symbol: defaultSymbols.hazeSymbol
                };
                hazeJson.symbol.color[3] = hazeOpac;
                const hazeGraphic = new esriBundle.Graphic(hazeJson);
                hazeGraphic.setGeometry(hgl._map.extent.expand(1.5)); // expand to avoid edges on quick pan
                hazeGraphic.haze = true;  // notifies layer to put this under any hilight graphics
                hgl.add(hazeGraphic);
            }

            // add new hilight graphics
            hgl._hilightGraphics = graphics;
            graphics.forEach(g => hgl.add(g));
            moveHilightToTop();
        };

        /**
        * Remove hilight from map
        * @method clearHilight
        */
        hgl.clearHilight = () => {
            // clear tracking vars, wipe the layer
            hgl._hilightGraphics = null;
            hgl.clear();
        };

        hgl.on('graphic-node-add', e => {
            // figure out if graphic needs to be at top or bottom of hilight layer
            // haze polygon goes to bottom, everything else to top
            const g = e.graphic;
            const dojoShape = g.getShape();

            if (g.haze) {
                dojoShape.moveToBack();
            } else {
                dojoShape.moveToFront();
            }
        });

        return hgl;
    };
}

module.exports = (esriBundle, geoApi) => ({
    makeHilightLayer: hilightBuilder(esriBundle),
    geomToGraphic: graphicBuilder(esriBundle),
    cloneLayerGraphic: cloneBuilder(esriBundle),
    getUnboundGraphics: getGraphicsBuilder(esriBundle, geoApi)
});