All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mware.web.product.map.util.layerHelpers.js Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of the BigConnect project.
 *
 * Copyright (c) 2013-2020 MWARE SOLUTIONS SRL
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * MWARE SOLUTIONS SRL, MWARE SOLUTIONS SRL DISCLAIMS THE WARRANTY OF
 * NON INFRINGEMENT OF THIRD PARTY RIGHTS
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the BigConnect software without
 * disclosing the source code of your own applications.
 *
 * These activities include: offering paid services to customers as an ASP,
 * embedding the product in a web application, shipping BigConnect with a
 * closed source product.
 */
define([
    'openlayers',
    '../multiPointCluster',
    'util/withDataRequest',
    './cache',
], function(
    ol,
    MultiPointCluster,
    withDataRequest,
    cache
) {
    'use strict';

    const FEATURE_CLUSTER_RADIUS = 12;
    const FEATURE_CLUSTER_RADIUS_MAX = 20;
    const VECTOR_FEATURE_SELECTION_OVERLAY = 'org-bigconnect-map-vector-selected-overlay';

    const DEFAULT_LAYER_CONFIG = {
        sortable: true,
        toggleable: true
    };

    const layers = {
        tile: {
            configure(id, options = {}) {
                const { source, sourceOptions = {}, ...config } = options;
                let baseLayerSource;

                if (source in ol.source && _.isFunction(ol.source[source])) {
                    baseLayerSource = new ol.source[source]({
                        crossOrigin: 'anonymous',
                        ...sourceOptions
                    });
                } else {
                    console.error('Unknown map provider type: ', source);
                    throw new Error('map.provider is invalid')
                }

                const layer = new ol.layer.Tile({
                    ...DEFAULT_LAYER_CONFIG,
                    id,
                    label: 'Base',
                    type: 'tile',
                    sortable: false,
                    source: baseLayerSource,
                    ...config
                });

                return { source: baseLayerSource, layer }
            },

            addEvents(map, { source }, handlers) {
                return [
                    source.on('tileloaderror', function(event) {
                        const MaxRetry = 3;
                        const { tile } = event;

                        if (tile) {
                            tile._retryCount = (tile._retryCount || 0) + 1;
                            if (tile._retryCount <= MaxRetry) {
                                console.warn(`Tile error retry: ${tile._retryCount} of ${MaxRetry}`, tile.src_);
                                _.defer(() => {
                                    tile.load();
                                })
                            }
                        }
                    })
                ]
            }
        },

        cluster: {
            configure(id, options = {}) {
                const source = new ol.source.Vector({ features: [] });
                const clusterSource = new MultiPointCluster({ source });
                const layer = new ol.layer.Vector({
                    ...DEFAULT_LAYER_CONFIG,
                    id,
                    label: 'Cluster',
                    type: 'cluster',
                    style: cluster => this.style(cluster, { source }),
                    source: clusterSource,
                    ...options
                });
                const heatmap = new ol.layer.Heatmap({
                    ...DEFAULT_LAYER_CONFIG,
                    ...options,
                    visible: false,
                    id: 'heatmap_cluster',
                    label: 'Heatmap',
                    type: 'cluster_heatmap',
                    source
                })

                cache.clear();

                return { source, clusterSource, layers: [heatmap, layer] }
            },

            style(cluster, { source, selected = false } = {}) {
                const count = cluster.get('count');
                const selectionState = cluster.get('selectionState') || 'none';
                const isSelected = selected || selectionState !== 'none';

                if (count > 1) {
                    return styles.cluster(cluster, { selected: isSelected, source });
                } else {
                    return styles.feature(cluster.get('features')[0], { selected: isSelected })
                }
            },

            addEvents(map, { source, clusterSource, layers }, handlers) {
                const [ heatmapLayer, vectorLayer ] = layers;
                const addToElements = list => feature => {
                    const el = feature.get('element');
                    const key = el.type === 'vertex' ? 'vertices' : 'edges';
                    list[key].push(el.id);
                };
                const isPartiallySelected = (cluster) => {
                    if (cluster.get('count') < 2) return false;

                    const features = cluster.get('features');
                    const selected = features.filter(f => f.get('selected'));
                    return 0 < selected.length && selected.length < features.length;
                };
                const getClusterFromEvent = ({ pixel }) => {
                    const pixelFeatures = map.getFeaturesAtPixel(pixel, {
                        layerFilter: layer => layer === vectorLayer
                    });
                    return pixelFeatures && pixelFeatures[0];
                };

                // For heatmap selections
                const onHeatmapClick = map.on('click', ({ pixel }) => {
                    const elements = { vertices: [], edges: [] };
                    map.forEachFeatureAtPixel(pixel, addToElements(elements), {
                        layerFilter: layer => layer === heatmapLayer
                    });

                    if (elements.vertices.length || elements.edges.length) {
                        handlers.onSelectElements(elements);
                    }
                });

                // For partial cluster selections
                const onClusterClick = map.on('click', event => {
                    const targetFeature = getClusterFromEvent(event);

                    if (targetFeature && isPartiallySelected(targetFeature)) {
                        const elements = { vertices: [], edges: [] };
                        const clusterIterator = addToElements(elements);

                        targetFeature.get('features').forEach(clusterIterator);
                        handlers.onAddSelection(elements);
                    }
                });

                //this does not support ol.interaction.Select.multi because of partial cluster selection
                const selectInteraction = new ol.interaction.Select({
                    addCondition: (event) => {
                        if (event.originalEvent.shiftKey) {
                            return true;
                        } else {
                            const targetFeature = getClusterFromEvent(event);
                            return !!targetFeature && isPartiallySelected(targetFeature);
                        }
                    },
                    condition: ol.events.condition.click,
                    toggleCondition: ol.events.condition.platformModifierKeyOnly,
                    layers: [vectorLayer],
                    style: cluster => this.style(cluster, { source, selected: true })
                });

                map.addInteraction(selectInteraction);

                const onSelectCluster = selectInteraction.on('select', function(event) {
                    const { selected, target: interaction } = event;
                    const clusters = interaction.getFeatures();
                    const elements = { vertices: [], edges: [] };
                    const clusterIterator = addToElements(elements);

                    clusters.forEach(cluster => {
                        let features = cluster.get('features');
                        if (isPartiallySelected(cluster) && !selected.includes(cluster)) {
                            features = features.filter(f => f.get('selected'));
                        }
                        features.forEach(clusterIterator);
                    });

                    handlers.onSelectElements(elements);
                });

                const onClusterSourceChange = clusterSource.on('change', _.debounce(function() {
                    var selected = selectInteraction.getFeatures(),
                        clusters = this.getFeatures(),
                        newSelection = [],
                        isSelected = feature => feature.get('selected');

                    clusters.forEach(cluster => {
                        var innerFeatures = cluster.get('features');
                        var all = true, some = false, count = 0;
                        innerFeatures.forEach(feature => {
                            const selected = isSelected(feature);
                            all = all && selected;
                            some = some || selected;
                            count += (selected ? 1 : 0)
                        })

                        if (some) {
                            newSelection.push(cluster);
                            cluster.set('selectionState', all ? 'all' : 'some');
                            cluster.set('selectionCount', count);
                        } else {
                            cluster.unset('selectionState');
                            cluster.unset('selectionCount');
                        }
                    })

                    selected.clear()
                    if (newSelection.length) {
                        selected.extend(newSelection)
                    }
                }, 100));

                return [
                    onHeatmapClick,
                    onClusterClick,
                    onSelectCluster,
                    onClusterSourceChange
                ]
            },

            update: syncFeatures
        },

        ancillary: {
            configure(id, options = {}, map) {
                const source = new ol.source.Vector({
                    features: [],
                    wrapX: false
                });
                if (options.getExtent) {
                    const _superExtent = source.getExtent;
                    source.getExtent = function() {
                        const extent = _superExtent && _superExtent.apply(this, arguments);
                        const customExtent = options.getExtent(map, source, extent);
                        if (ol.extent.isEmpty(customExtent)) {
                            return extent || ol.extent.createEmpty();
                        }
                        return customExtent || extent || ol.extent.createEmpty();
                    };
                }
                const layer = new ol.layer.Vector({
                    ...DEFAULT_LAYER_CONFIG,
                    id,
                    type: 'ancillary',
                    sortable: false,
                    toggleable: false,
                    source,
                    renderBuffer: 500,
                    updateWhileInteracting: true,
                    updateWhileAnimating: true,
                    style: ancillary => this.style(ancillary),
                    ...options
                });

                return { source, layer }
            },

            style(ancillary) {
                const extensionStyles = ancillary.get('styles');
                if (extensionStyles) {
                    const { normal } = extensionStyles;
                    if (normal.length) {
                        return normal;
                    }
                }
            },

            update: syncFeatures
        },

        vectorXhr: {
            configure(id, options = {}) {
                const { sourceOptions = {}, ...layerOptions } = options;
                const source = new ol.source.Vector(sourceOptions);
                const layer = new ol.layer.Vector({
                    ...DEFAULT_LAYER_CONFIG,
                    id,
                    type: 'vectorXhr',
                    source,
                    ...layerOptions
                });

                return { source, layer };
            },

            addEvents(map, { source: olSource, layer }, handlers) {
                const elements = { vertices: [], edges: [] };
                const element = layer.get('element');
                const key = element.type === 'vertex' ? 'vertices' : 'edges';
                const overlayId = getOverlayIdForLayer(layer);

                elements[key].push(element.id);

                const onGeoShapeClick = map.on('click', (e) => {
                    const { map, pixel } = e;
                    const featuresAtPixel = map.getFeaturesAtPixel(pixel);
                    const sourceFeatures = olSource.getFeatures();

                    if (featuresAtPixel) {
                        if (featuresAtPixel.length === 1
                            && featuresAtPixel[0].getId() === overlayId
                            && olSource.getFeatureById(overlayId)) {
                            handlers.onSelectElements({ vertices: [], edges: [] });
                        } else if (featuresAtPixel.every(feature => sourceFeatures.includes(feature))) {
                            handlers.onSelectElements(elements);
                        }
                    }
                });

                const onLayerFeaturesLoaded = olSource.on('propertyChange', (e) => {
                    if (e.key === 'status' && e.target.get(e.key) === 'loaded') {
                        const selectionOverlay = olSource.getFeatureById(overlayId);

                        if (selectionOverlay) {
                            let extent;

                            olSource.forEachFeature(feature => {
                                const geom = feature.getGeometry();
                                const featureExtent = geom.getExtent();

                                if (feature.getId() !== overlayId) {
                                    if (extent) {
                                        ol.extent.extend(extent, featureExtent);
                                    } else {
                                        extent = featureExtent;
                                    }
                                }
                            });

                            const geometry = ol.geom.Polygon.fromExtent(extent);
                            selectionOverlay.setGeometry(geometry);
                        }
                    }
                });

                return [ onGeoShapeClick, onLayerFeaturesLoaded ]
            },

            update(source, { source: olSource, layer }) {
                const { element, features, selected } = source;
                const layerStatus = layer.get('status');
                const nextFeatures = [];
                let changed = false;
                let fitFeatures;

                if (element !== layer.get('element')) {
                    olSource.set('element', element);
                    changed = true;
                }

                if (!layerStatus) {
                    this.loadFeatures(olSource, layer).then((features) => {
                        if (features) {
                            olSource.clear(true)
                            olSource.addFeatures(features);
                            layer.set('status', 'loaded');
                        }
                    });
                } else if (selected !== olSource.get('selected')) {
                    const overlayId = getOverlayIdForLayer(layer);
                    olSource.set('selected', selected);
                    changed = true;

                    if (selected && layerStatus === 'loaded') {
                        let extent;
                        olSource.forEachFeature(feature => {
                            const geom = feature.getGeometry();
                            const featureExtent = geom.getExtent();

                            if (feature.getId() !== overlayId) {
                                if (extent) {
                                    ol.extent.extend(extent, featureExtent);
                                } else {
                                    extent = featureExtent;
                                }
                            }
                        });

                        const selectedOverlay = new ol.Feature(ol.geom.Polygon.fromExtent(extent || [0, 0, 0, 0]));
                        selectedOverlay.setStyle(new ol.style.Style({
                            fill: new ol.style.Fill({ color: [0, 136, 204, 0.3] }),
                            stroke: new ol.style.Stroke({ color: [0, 136, 204, 0.4], width: 1 })
                        }));
                        selectedOverlay.setId(overlayId)

                        olSource.addFeature(selectedOverlay);
                    } else {
                        const selectedOverlay = olSource.getFeatureById(overlayId);
                        if (selectedOverlay) {
                            olSource.removeFeature(selectedOverlay);
                        }
                    }
                }

                return { changed };
            },

            loadFeatures(olSource, layer) {
                const { id, element, propName, propKey, mimeType } = layer.getProperties();

                layer.set('status', 'loading');

                return withDataRequest.dataRequest('vertex', 'propertyValue', id, propName, propKey).then(source => {
                    const format = getFormatForMimeType(mimeType);
                    const dataProjection = format.readProjection(source);

                    if (!dataProjection || !ol.proj.get(dataProjection.getCode())) {
                        throw new Error('unhandledDataProjection');
                    } else {
                        const features = format.readFeatures(source, {
                            dataProjection,
                            featureProjection: 'EPSG:3857'
                        });

                        return features;
                    }

                })
                    .then(features => {
                        return features.map((feature, i) => {
                            feature.setId(`${layer.get('id')}:${i}`)
                            feature.set('element', element)

                            return feature
                        })
                    })
                    .catch(e => {
                        const message = e.message === 'unhandledDataProjection'
                            ? i18n('org.bigconnect.web.product.map.MapWorkProduct.layer.error.data.format')
                            : i18n('org.bigconnect.web.product.map.MapWorkProduct.layer.error');

                        layer.set('status', { type: 'error', message });
                    });
            }
        }
    };


    const styles = {
        feature(feature, { selected = false } = {}) {
            const {
                focused,
                focusedDim,
                styles: extensionStyles,
                selected: featureSelected,
                _nodeRadius: radius
            } = feature.getProperties();

            const isSelected = selected || featureSelected;
            let needSelectedStyle = true;
            let needFocusStyle = true;
            let styleList;

            if (extensionStyles) {
                const { normal: normalStyle, selected: selectedStyle } = extensionStyles;
                let style;
                if (normalStyle.length && (!isSelected || !selectedStyle.length)) {
                    style = normalStyle;
                } else if (selectedStyle.length && isSelected) {
                    style = selectedStyle;
                }

                if (style) {
                    styleList = _.isArray(style) ? style : [style];
                }
            } else {
                needSelectedStyle = false;
                needFocusStyle = false;
                styleList = cache.getOrCreateFeature({
                    src: feature.get(isSelected ? 'iconUrlSelected' : 'iconUrl'),
                    imgSize: feature.get('iconSize'),
                    scale: 1 / feature.get('pixelRatio'),
                    anchor: feature.get('iconAnchor')
                }, focused)
            }

            if (_.isEmpty(styleList)) {
                console.warn('No styles for feature, ignoring.', feature);
                return [];
            }

            if (needFocusStyle && focused) {
                return cache.addFocus(radius, cache.reset(radius, styleList));
            }
            if (focusedDim) {
                return cache.addDim(radius, styleList)
            }

            return cache.reset(radius, styleList);
        },

        cluster(cluster, { selected = false, source, clusterSource } = {}) {
            var count = cluster.get('count'),
                focusStats = cluster.get('focusStats'),
                selectionState = cluster.get('selectionState') || 'none',
                selectionCount = cluster.get('selectionCount') || 0,
                { min, max } = source.countStats,
                value = Math.min(max, Math.max(min, count)),
                radius = min === max ?
                    FEATURE_CLUSTER_RADIUS :
                    interpolate(value, min, max, FEATURE_CLUSTER_RADIUS, FEATURE_CLUSTER_RADIUS_MAX);

            return cache.getOrCreateCluster({
                count, radius, selected, selectionState, selectionCount, focusStats
            })
        }
    };

    function interpolate(v, x0, x1, y0, y1) {
        return (y0 * (x1 - v) + y1 * (v - x0)) / (x1 - x0)
    }

    function setLayerConfig(config = {}, layer) {
        const { visible = true, opacity = 1, zIndex = 0, ...properties } = config;

        _.mapObject(properties, (value, key) => {
            if (value === null) {
                layer.unset(key);
            } else {
                layer.set(key, value);
            }
        })

        layer.setVisible(visible)
        layer.setOpacity(opacity)
        layer.setZIndex(zIndex)
    }

    function syncFeatures({ features }, { source }, focused) {
        const existingFeatures = _.indexBy(source.getFeatures(), f => f.getId());
        const newFeatures = [];
        var changed = false;

        if (features) {
            for (let featureIndex = 0; featureIndex < features.length; featureIndex++) {
                const data = features[featureIndex];
                const { id, styles, geometry: geometryOverride, geoLocations, element, ...rest } = data;
                let geometry = null;

                if (geometryOverride) {
                    geometry = geometryOverride;
                } else if (geoLocations) {
                    geometry = cache.getOrCreateGeometry(id, geoLocations);
                }

                if (geometry) {
                    let featureValues = {
                        ...rest,
                        element,
                        geoLocations,
                        geometry
                    };

                    if (styles) {
                        const { normal, selected } = styles;
                        if (normal && normal.length) {
                            const radius = getRadiusFromStyles(normal);
                            const normalImage = _.isFunction(normal[0].getImage) &&
                                normal[0].getImage();

                            featureValues._nodeRadius = radius

                            if (selected.length === 0 && !geometryOverride && normalImage && _.isFunction(normalImage.getStroke)) {
                                const newSelected = normal[0].clone();
                                const prevStroke = normal[0].getImage().getStroke();
                                const newStroke = new ol.style.Stroke({
                                    color: '#0088cc',
                                    width:  prevStroke && prevStroke.getWidth() || 1
                                })

                                newSelected.image_ = normal[0].getImage().clone({
                                    stroke: newStroke,
                                    opacity: 1
                                });

                                featureValues.styles = {
                                    normal,
                                    selected: [newSelected]
                                }
                            } else {
                                featureValues.styles = styles;
                            }
                        }
                    }

                    if (focused && focused.isFocusing) {
                        if (element.id in focused[element.type === 'vertex' ? 'vertices' : 'edges']) {
                            featureValues.focused = true
                            featureValues.focusedDim = false
                        } else {
                            featureValues.focused = false
                            featureValues.focusedDim = true
                        }
                    } else {
                        featureValues.focused = false
                        featureValues.focusedDim = false
                    }

                    if (id in existingFeatures) {
                        const existingFeature = existingFeatures[id];
                        let diff = _.any(existingFeature.getProperties(), (val, name) => {
                            switch (name) {
                                case 'styles':
                                case 'interacting':
                                    return false
                                case 'geoLocations':
                                    return !_.isEqual(val, featureValues[name])
                                default:
                                    return val !== featureValues[name]
                            }
                        })

                        if (diff) {
                            changed = true
                            if (existingFeature.get('interacting')) {
                                delete featureValues.geometry;
                            }
                            existingFeature.setProperties(featureValues)
                        }
                        delete existingFeatures[id];
                    } else {
                        var feature = new ol.Feature(featureValues);
                        feature.setId(data.id);
                        newFeatures.push(feature);
                    }
                }
            }
        }

        let fitFeatures;
        if (newFeatures.length) {
            changed = true
            source.addFeatures(newFeatures);
            fitFeatures = newFeatures;
        }
        if (!_.isEmpty(existingFeatures)) {
            changed = true
            _.forEach(existingFeatures, feature => source.removeFeature(feature));
        }
        return { changed, fitFeatures };
    }

    function getFormatForMimeType(mimeType) {
        switch (mimeType) {
            case 'application/vnd.geo+json':
                return new ol.format.GeoJSON();
            case 'application/vnd.google-earth.kml+xml':
                return new ol.format.KML();
        }
    }

    function getOverlayIdForLayer(layer) {
        return layer.get('id') + '|' + VECTOR_FEATURE_SELECTION_OVERLAY;
    }

    function getRadiusFromStyles(styles) {
        for (let i = styles.length - 1; i >= 0; i--) {
            if (_.isFunction(styles[i].getImage)) {
                const image = styles[i].getImage();
                const radius = image && _.isFunction(image.getRadius) && image.getRadius();

                if (radius) {
                    const nodeRadius = radius / devicePixelRatio
                    return nodeRadius;
                }
            }
        }
    }

    return {
        byType: layers,
        styles,
        setLayerConfig
    }
})




© 2015 - 2024 Weber Informatics LLC | Privacy Policy