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

com.mware.web.product.map.OpenLayers.jsx 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([
    'create-react-class',
    'prop-types',
    'openlayers',
    './util/layerHelpers',
    'product/toolbar/ProductToolbar'
], function(
    createReactClass,
    PropTypes,
    ol,
    layerHelpers,
    ProductToolbar) {

    const noop = function() {};

    const ANIMATION_DURATION = 200,
        MIN_FIT_ZOOM_RESOLUTION = 30,
        MAX_FIT_ZOOM_RESOLUTION = 20000,
        PREVIEW_WIDTH = 300,
        PREVIEW_HEIGHT = 300,
        PREVIEW_DEBOUNCE_SECONDS = 2,
        LAYERS_EXTENDED_DATA_KEY = 'org-bigconnect-map-layers',
        BASE_LAYER_ID = 'base';

    const OpenLayers = createReactClass({
        propTypes: {
            product: PropTypes.object.isRequired,
            baseSource: PropTypes.string.isRequired,
            baseSourceOptions: PropTypes.object,
            sourcesByLayerId: PropTypes.object,
            generatePreview: PropTypes.bool,
            onSelectElements: PropTypes.func.isRequired,
            onUpdatePreview: PropTypes.func.isRequired,
            onTap: PropTypes.func,
            onContextTap: PropTypes.func,
            onZoom: PropTypes.func,
            onPan: PropTypes.func,
            onMouseOver: PropTypes.func,
            onMouseOut: PropTypes.func
        },

        getInitialState() {
            return { panning: false }
        },

        getDefaultProps() {
            return {
                generatePreview: false,
                onTap: noop,
                onContextTap: noop,
                onZoom: noop,
                onPan: noop
            }
        },

        componentWillReceiveProps(nextProps) {
            const { sourcesByLayerId: prevSourcesByLayerId, product: prevProduct } = this.props;
            const {
                sourcesByLayerId: nextSourcesByLayerId,
                product: nextProduct,
                registry,
                layerExtensions } = nextProps;
            const { map, layersWithSources } = this.state;
            const nextLayerIds = Object.keys(nextProps.sourcesByLayerId);

            if (layersWithSources && (
                    nextLayerIds.length !== Object.keys(layersWithSources).length
                    || nextLayerIds.some(layerId => !layersWithSources[layerId])
                )) {
                const previous = Object.keys(prevSourcesByLayerId);
                const newLayers = [];
                const nextLayers = map.getLayerGroup().getLayers().getArray().slice(0);
                const existingLayersById = _.indexBy(nextLayers, layer => layer.get('id'));
                const newLayersWithSources = {};
                const addLayer = (initializer, layerId, options, map) => {
                    const layerWithSource = this.initializeLayer(initializer, layerId, options, map);
                    const layers = layerWithSource.layers || [layerWithSource.layer];

                    layers.forEach(layer => {
                        const config = nextProps.layerConfig && nextProps.layerConfig[layer.get('id')];
                        if (config) {
                            layerHelpers.setLayerConfig(config, layer);
                        }

                        newLayersWithSources[layerId] = layerWithSource;
                        nextLayers.push(layer);
                    })

                };

                Object.keys(nextSourcesByLayerId).forEach((layerId) => {
                    if (!prevSourcesByLayerId[layerId]) {
                        newLayers.push(layerId);
                    } else {
                        const layerIndex = previous.indexOf(layerId);
                        previous.splice(layerIndex, 1);
                    }
                });

                previous.forEach(layerId => {
                    const layerIndex = nextLayers.findIndex(layer => layer.get('id') === layerId);
                    nextLayers.splice(layerIndex, 1);
                });

                newLayers.forEach(layerId => {
                    if (!existingLayersById[layerId]) {
                        const { type, features, ...options } = nextSourcesByLayerId[layerId];
                        const initializer = layerHelpers.byType[type] || registry['org.bigconnect.map.layer'].find(e => e.type === type);

                        if (initializer) {
                            addLayer(initializer, layerId, options, map)
                        } else {
                            console.warn('Sources present for layer: ' + layerId + ', but no layer type defined for: ' + type);
                        }
                    }
                });

                if (nextProduct.id !== prevProduct.id) {
                    _.mapObject(layerExtensions, (e, layerId) => {
                        if (!(layerId in newLayersWithSources) && !(layerId in nextSourcesByLayerId)) {
                            addLayer(e, layerId, e.options, map);
                        }
                    })
                }

                map.getLayerGroup().setLayers(new ol.Collection(nextLayers));

                if (previous.length || Object.keys(newLayersWithSources).length) {
                    this.setState({ layersWithSources: {
                        ..._.omit(layersWithSources, previous),
                        ...newLayersWithSources
                    }});
                }
            }
        },

        componentDidUpdate(prevProps, prevState) {
            const { map, layersWithSources } = this.state;
            const { product, sourcesByLayerId, layerExtensions, layerConfig, focused, viewport, generatePreview } = this.props;

            let changed = false;
            let fit = [];

            const layers = map.getLayers();

            layers.forEach(layer => {
                const layerId = layer.get('id');

                const layerType = layer.get('type');
                const layerHelper = layerHelpers.byType[layerType] || layerExtensions[layerId];
                const layerWithSources = layersWithSources[layerId];
                const nextSource = sourcesByLayerId[layerId];
                const prevSource = prevProps.sourcesByLayerId[layerId];

                if (layerHelper && layerWithSources) {
                    const shouldUpdate = _.isFunction(layerHelper.shouldUpdate)
                        ? layerHelper.shouldUpdate(nextSource, prevSource, layerWithSources, focused)
                        : true;

                    if (shouldUpdate && _.isFunction(layerHelper.update) && nextSource) {
                        const { changed: c = true, fitFeatures = [] } = layerHelper.update(nextSource, layerWithSources, focused) || {};
                        changed = changed || c;
                        if (fitFeatures) fit.push(...fitFeatures)
                    }
                }
            });

            const newLayerOrder = product.extendedData
                && product.extendedData[LAYERS_EXTENDED_DATA_KEY]
                && product.extendedData[LAYERS_EXTENDED_DATA_KEY].layerOrder;
            const prevLayerOrder = prevProps.product.extendedData
                && prevProps.product.extendedData[LAYERS_EXTENDED_DATA_KEY]
                && prevProps.product.extendedData[LAYERS_EXTENDED_DATA_KEY].layerOrder;
            if (map && (map !== prevState.map || newLayerOrder !== prevLayerOrder)
                || prevState.layersWithSources.length !== Object.keys(layersWithSources).length
                || prevState.layersWithSource.some(layerId => !layersWithSources[layerId])) {
                this.applyLayerOrder();
            }

            if (fit.length) {
                this.fit({ limitToFeatures: fit });
            }

            if (viewport && !_.isEmpty(viewport)) {
                map.getView().setCenter(viewport.pan);
                map.getView().setResolution(viewport.zoom);
            }

            if (map && (!prevState.map || prevProps.layerConfig !== layerConfig)) {
                this.applyLayerConfig();
            }

            if (generatePreview) {
                this._updatePreview({ fit: !viewport });
            } else if (changed) {
                this.updatePreview();
            }
        },

        _updatePreview(options = {}) {
            const { fit = false } = options;
            const { map, layersWithSources } = this.state;
            const { base } = layersWithSources;
            const doFit = () => {
                if (fit) this.fit({ animate: false });
            };

            // Since this is delayed, make sure component not unmounted
            if (!this._canvasPreviewBuffer) return;

            doFit();
            map.once('postcompose', (event) => {
                if (!this._canvasPreviewBuffer) return;
                var loading = 0, loaded = 0, events, captureTimer;

                doFit();

                const mapCanvas = event.context.canvas;
                const capture = _.debounce(() => {
                    if (!this._canvasPreviewBuffer) return;

                    doFit();

                    map.once('postrender', () => {
                        if (!this._canvasPreviewBuffer) return;
                        var newCanvas = this._canvasPreviewBuffer;
                        var context = newCanvas.getContext('2d');
                        var hRatio = PREVIEW_WIDTH / mapCanvas.width;
                        var vRatio = PREVIEW_HEIGHT / mapCanvas.height;
                        var ratio = Math.min(hRatio, vRatio);
                        newCanvas.width = Math.trunc(mapCanvas.width * ratio);
                        newCanvas.height = Math.trunc(mapCanvas.height * ratio);
                        context.drawImage(mapCanvas,
                            0, 0, mapCanvas.width, mapCanvas.height,
                            0, 0, newCanvas.width, newCanvas.height
                        );
                        if (events) {
                            events.forEach(key => ol.Observable.unByKey(key));
                        }
                        this.props.onUpdatePreview(newCanvas.toDataURL('image/png'));
                    });
                    map.renderSync();
                }, 100)

                const tileLoadStart = () => {
                    clearTimeout(captureTimer);
                    ++loading;
                };
                const tileLoadEnd = (event) => {
                    clearTimeout(captureTimer);
                    if (loading === ++loaded) {
                        captureTimer = capture();
                    }
                };

                events = [
                    base.source.on('tileloadstart', tileLoadStart),
                    base.source.on('tileloadend', tileLoadEnd),
                    base.source.on('tileloaderror', tileLoadEnd)
                ];
            });
            map.renderSync();
        },

        componentDidMount() {
            this._canvasPreviewBuffer = document.createElement('canvas');
            this._canvasPreviewBuffer.width = PREVIEW_WIDTH;
            this._canvasPreviewBuffer.height = PREVIEW_HEIGHT;

            this.olEvents = [];
            this.domEvents = [];
            this.updatePreview = _.debounce(this._updatePreview, PREVIEW_DEBOUNCE_SECONDS * 1000);
            const { map, layersWithSources } = this.configureMap();

            this.setState({ map, layersWithSources })
        },

        componentWillUnmount() {
            this._canvasPreviewBuffer = null;
            clearTimeout(this._handleMouseMoveTimeout);
            if (this.domEvents) {
                this.domEvents.forEach(fn => fn());
                this.domEvents = null;
            }
            if (this.olEvents) {
                this.olEvents.forEach(key => ol.Observable.unByKey(key));
                this.olEvents = null;
            }
        },

        render() {
            // Cover the map when panning/dragging to avoid sending events there
            const moveWrapper = this.state.panning ? (
) : ''; return (
{moveWrapper}
) }, onControlsFit() { this.fit(); }, onControlsZoom(type) { const { map } = this.state; const view = map.getView(); if (!this._slowZoomIn) { this._slowZoomIn = _.throttle(zoomByDelta(1), ANIMATION_DURATION, {trailing: false}); this._slowZoomOut = _.throttle(zoomByDelta(-1), ANIMATION_DURATION, {trailing: false}); } if (type === 'in') { this._slowZoomIn(); } else { this._slowZoomOut(); } function zoomByDelta(delta) { return () => { var currentResolution = view.getResolution(); if (currentResolution) { view.animate({ resolution: view.constrainResolution(currentResolution, delta), duration: ANIMATION_DURATION }); } } } }, onControlsPan({ x, y }, { state }) { if (state === 'panningStart') { this.setState({ panning: true }) } else if (state === 'panningEnd') { this.setState({ panning: false }) } else { const { map } = this.state; const view = map.getView(); var currentCenter = view.getCenter(), resolution = view.getResolution(), center = view.constrainCenter([ currentCenter[0] - x * resolution, currentCenter[1] + y * resolution ]); view.setCenter(center); } }, extentFromFeatures(features) { const extent = ol.extent.createEmpty(); features.forEach(feature => { const fExtent = feature.getGeometry().getExtent(); if (!ol.extent.isEmpty(fExtent)) { ol.extent.extend(extent, fExtent); } }); return extent; }, fit(options = {}) { const { animate = true, limitToFeatures = [] } = options; const { map, layersWithSources } = this.state; const view = map.getView(); const changeZoom = limitToFeatures.length !== 1; let extent; if (limitToFeatures.length) { extent = this.extentFromFeatures(limitToFeatures) } else { extent = ol.extent.createEmpty(); map.getLayers().forEach(layer => { const source = layersWithSources.cluster.layers.includes(layer) ? layersWithSources.cluster.source : layer.getSource(); if (layer.getVisible()) { if (_.isFunction(source.getExtent)) { ol.extent.extend(extent, source.getExtent()); } } }) } if (!ol.extent.isEmpty(extent)) { var resolution = view.getResolution(), extentWithPadding = extent, { left, right, top, bottom } = this.props.panelPadding, clientBox = this.refs.map.getBoundingClientRect(), padding = 20, viewportWidth = clientBox.width - left - right - padding * 2, viewportHeight = clientBox.height - top - bottom - padding * 2, extentWithPaddingSize = ol.extent.getSize(extentWithPadding), currentExtent = view.calculateExtent([viewportWidth, viewportHeight]), // Figure out ideal resolution based on available realestate idealResolution = Math.max( extentWithPaddingSize[0] / viewportWidth, extentWithPaddingSize[1] / viewportHeight ); if (limitToFeatures.length) { const horizontalSync = ((left + padding) / 2 - (right + padding) / 2) * resolution; const verticalSync = ((top + padding) / 2 - (bottom + padding) / 2) * resolution; currentExtent[0] += horizontalSync; currentExtent[1] += verticalSync; currentExtent[2] += horizontalSync; currentExtent[3] += verticalSync; var insideCurrentView = ol.extent.containsExtent(currentExtent, extent); if (insideCurrentView) { return; } } const newResolution = changeZoom ? view.constrainResolution( Math.min(MAX_FIT_ZOOM_RESOLUTION, Math.max(idealResolution, MIN_FIT_ZOOM_RESOLUTION)), -1 ) : view.getResolution(); const center = ol.extent.getCenter(extentWithPadding); const offsetX = left - right; const offsetY = top - bottom; const lon = offsetX * newResolution / 2; const lat = offsetY * newResolution / 2; center[0] = center[0] - lon; center[1] = center[1] - lat; const options = { center }; if (changeZoom) { options.resolution = newResolution; } view.animate({ ...options, duration: animate ? ANIMATION_DURATION : 0 }) } else { view.animate({ ...this.getDefaultViewParameters(), duration: animate ? ANIMATION_DURATION : 0 }); } }, getDefaultViewParameters() { return { zoom: 2, minZoom: 1, center: [0, 0] }; }, configureMap() { const { baseSource, baseSourceOptions = {}, sourcesByLayerId, layerExtensions } = this.props; const layersWithSources = {}; const addLayer = (layerExtension, id, options, map) => { if (layersWithSources[id]) return; const layerWithSource = this.initializeLayer(layerExtension, id, options, map); const layers = layerWithSource.layers || [layerWithSource.layer]; layers.forEach(l => { map.addLayer(l); }); layersWithSources[id] = layerWithSource; }; const map = new ol.Map({ loadTilesWhileInteracting: true, keyboardEventTarget: document, controls: [], layers: [], target: this.refs.map }); // add the base(tile) layer addLayer(layerHelpers.byType.tile, BASE_LAYER_ID, { source: baseSource, sourceOptions: baseSourceOptions }, map); // add layers from org.bigconnect.map.layer registered extensions _.mapObject(layerExtensions, (extension, layerId) => { addLayer(extension, layerId, extension.options, map) }); // add layers from sources passed in props _.mapObject(sourcesByLayerId, ({ type, features, ...options }, layerId) => { const initializer = layerHelpers.byType[type]; if (initializer) { addLayer(initializer, layerId, options, map); } else { console.warn('Sources present for layer: ' + layerId + ', but no layer type defined for: ' + type); } }); this.configureEvents(map); const view = new ol.View(this.getDefaultViewParameters()); this.olEvents.push(view.on('change:center', (event) => this.props.onPan(event))); this.olEvents.push(view.on('change:resolution', (event) => this.props.onZoom(event))); map.setView(view); return { map, layersWithSources} }, configureEvents(map) { var self = this; this.olEvents.push(map.on('click', function(event) { self.props.onTap(event); })); this.olEvents.push(map.on('pointerup', function(event) { const { pointerEvent } = event; if (pointerEvent && pointerEvent.button === 2) { self.props.onContextTap(ol, event); } })); const viewport = map.getViewport(); this.domEvent(viewport, 'contextmenu', function(event) { event.preventDefault(); }) this.domEvent(viewport, 'mouseup', function(event) { event.preventDefault(); if (event.button === 2 || event.ctrlKey) { // TODO //self.handleContextMenu(event); } }); this.domEvent(viewport, 'mousemove', event => { const pixel = map.getEventPixel(event); const hit = map.getFeaturesAtPixel(pixel); if (hit) { this.handleMouseMove(hit); map.getTarget().style.cursor = 'pointer'; } else { this.handleMouseMove(); map.getTarget().style.cursor = ''; } }); }, initializeLayer(layerExtension, layerId, options, map) { const { baseSource, baseSourceOptions, sourcesByLayerId, generatePreview, layerExtensions, layerConfig, ...handlers } = this.props; const layerHelper = layerExtension.type && layerHelpers.byType[layerExtension.type] || layerExtension; const layerWithSource = layerHelper.configure(layerId, options, map); if (_.isFunction(layerHelper.addEvents)) { this.olEvents.concat(layerHelper.addEvents(map, layerWithSource, handlers)); } const layers = layerWithSource.layers || [layerWithSource.layer]; layers.forEach(layer => { const config = layerConfig && layerConfig[layer.get('id')]; if (config) { layerHelpers.setLayerConfig(config, layer); } }); return layerWithSource; }, applyLayerOrder() { const { map } = this.state; const { product, setLayerOrder } = this.props; const layersById = _.indexBy(map.getLayers().getArray(), layer => layer.get('id')); const nextLayerGroup = map.getLayerGroup(); let layerOrder = product.extendedData && product.extendedData[LAYERS_EXTENDED_DATA_KEY] && product.extendedData[LAYERS_EXTENDED_DATA_KEY].layerOrder.slice(0) || []; let orderedLayers = new ol.Collection(); let newLayers = []; orderedLayers.push(layersById[BASE_LAYER_ID]); delete layersById[BASE_LAYER_ID]; if (layerOrder.length) { layerOrder = layerOrder.reverse(); layerOrder.forEach((layerId, i) => { const layer = layersById[layerId]; if (layer) { orderedLayers.push(layer); delete layersById[layerId]; } }); _.mapObject(layersById, (layer, layerId) => { orderedLayers.push(layer); newLayers.push(layerId); }); nextLayerGroup.setLayers(orderedLayers); } else { newLayers = map.getLayers().getArray().slice(1).reduce((ids, layer) => { ids.push(layer.get('id')); return ids; }, []); } if (newLayers.length) { setLayerOrder(layerOrder.concat(newLayers.reverse())) } }, applyLayerConfig() { const map = this.state.map; const layerConfig = this.props.layerConfig; if (layerConfig) { const layersById = _.indexBy(map.getLayers().getArray(), layer => layer.get('id')); _.mapObject(layersById, (layer, layerId) => { const config = layerConfig[layerId]; layerHelpers.setLayerConfig(config, layersById[layerId]); }); } }, domEvent(el, type, handler) { this.domEvents.push(() => el.removeEventListener(type, handler)); el.addEventListener(type, handler, false); }, handleMouseMove(features) { const { onMouseOver, onMouseOut } = this.props; const { map } = this.state; if (!onMouseOver && !onMouseOut ) { return; } const stillHoveringSameFeature = features && this._handleMouseMoveFeatures && this._handleMouseMoveFeatures.length === features.length && this._handleMouseMoveFeatures[0] === features[0]; if (!stillHoveringSameFeature) { clearTimeout(this._handleMouseMoveTimeout); if (features && features.length) { this._handleMouseMoveTimeout = setTimeout(() => { this._handleMouseMoveFeatures = features; if (onMouseOver) { onMouseOver(ol, map, this._handleMouseMoveFeatures) } }, 250); } else if (this._handleMouseMoveFeatures) { if (onMouseOut) { onMouseOut(ol, map, this._handleMouseMoveFeatures) } this._handleMouseMoveFeatures = null; } } }, /** * Map work product toolbar item component * * @typedef org.bigconnect.product.toolbar.item~MapComponent * @property {function} requestUpdate Reload the maps extensions and styles. * Call when the result of extensions will change from variables * outside of inputs (preferences, etc). * @property {object} product The map product * @property {object} ol The [Openlayers Api](http://openlayers.org/en/latest/apidoc/) * @property {object} map [map](http://openlayers.org/en/latest/apidoc/ol.Map.html) instance * @property {Object.} layersWithSources Keyed by the id of the layer, the map's rendered layers with their sources * @property {object} cluster deprecated, access this from inside {@link org.bigconnect.product.toolbar.item~layersWithSources} instead * @property {object} cluster.clusterSource [multiPointCluster] that implements the [`ol.source.Cluster`](http://openlayers.org/en/latest/apidoc/ol.source.Cluster.html) interface to cluster the `source` features. * @property {object} cluster.source The [`ol.source.Vector`](http://openlayers.org/en/latest/apidoc/ol.source.Vector.html) source of all map pins before clustering. * @property {object} cluster.layer The [`ol.layer.Vector`](http://openlayers.org/en/latest/apidoc/ol.layer.Vector.html) pin layer */ getInjectedToolProps() { const { clearCaches: requestUpdate, product } = this.props; const { map, layersWithSources } = this.state; let props = {}; if (map && layersWithSources) { /** * @typedef {object} org.bigconnect.product.toolbar.item~layerWithSource * @property {object} source The [`ol.source`](http://openlayers.org/en/latest/apidoc/ol.source.html) of the layer * @property {object} layer The [`ol.layer`](http://openlayers.org/en/latest/apidoc/ol.layer.html) rendered in the map */ /** * @typedef {object.} org.bigconnect.product.toolbar.item~layersWithSources * * Keyed by layerId, the map's rendered sources with the layers they are backing */ props = { product, ol, map, cluster: layersWithSources.cluster, layersWithSources, requestUpdate } } return props; } }) return OpenLayers; })




© 2015 - 2024 Weber Informatics LLC | Privacy Policy