Source: layers/vector.js

let AbstractLayer = require('./abstract');

let ol = require('openlayers');
let identifyResult = require('../../modules/tasks/identifyResult');
let searchResult = require('../../modules/tasks/searchResult');
let fuse = require('fuse.js');
let DataSource = require('./../dataSources/dataSource');
let Interaction = require('./../interactions/interaction');

let DefaultStyle = require('./../styles/defaultStyle');
let LegendItem = require('./../styles/legendItem');
let SymbolExample = require('./../styles/symbolExample');
let ExtractStyle = require('./../styles/extractStyle');

/**
 * Класс слоя типа VectorLayer - рендеринг производится на стороне клиента на основе сведений о геометрии и атрибутах объектов,
 * а также назначенной функции стилизации (рендерера)
 * @extends AbstractLayer
 */
class VectorLayer extends AbstractLayer {
    /**
     * Создание экземляра слоя типа VectorLayer
     * @param params
     * @param params.url - адрес геосервера
     * @param params.className - имя слоя
     * @param params.mappings {object} - словарь полей (свойств) слоя
     * @returns {ol.layer.Vector}
     */
    constructor(params) {
        super(params);
        this.initStore(params);
    }

    /**
     * @param {object} params
     * @param params.className {string} - имя класса
     * @param params.mappings {object} - объект, содержащий трансляцию полей (подстановка)
     * @param params.displayField {string} - подписываемое поле
     */
    initStore(params) {
        let self = this;
        self.events = {};
        self.store = {};
        self.store.id = params.id;
        self.store.alias = params.alias;
        self.store.url = params.url;
        self.store.className = params.className;
        self.store.alias = params.alias;
        self.store.layerType = "opengis.layers.vector";
        self.store.mappings = params.mappings || {};
        self.interactions = {};

        self.store.displayField = testDisplayField(params.displayField);

        // источник данных
        self.store.sourceFabric = createSourceFabric(params);

        //todo
        params.sourceFabric = self.store.sourceFabric;

        self.layer = createLayer(params);
        self.store.layer = self.layer;

        self.store.params = params;

        //todo сделать рефакторинг
        self.store.params.sourceFabric = self.store.sourceFabric;
    }

    /**
     * Идентификация объектов на слое
     * @param params
     * @return {*|Array.<ol.Feature|ol.render.Feature>|ol.Collection.<ol.Feature>|Array.<ol.Feature>}
     */
    identify(params) {
        let self = this;
        let geometry = params.geometry || params.coordinate;
        let radius = params.radius || 20;
        let extent = [geometry[0] - radius, geometry[1] - radius, geometry[0] + radius, geometry[1] + radius];
        let store = this.store;
        let source = store.layer.getSource();

        let features = self.getFeatures();
        features = features.map(function (item) {
            let featureAttributes = item.getProperties();
            let featureGeometry = item.getGeometry();
            delete featureAttributes.geometry;

            let resultAttributes = generateField(featureAttributes, store);

            return new identifyResult({
                layerId: store.id,
                layerName: store.alias,
                queryGeometry: geometry,
                attributes: featureAttributes,
                preparedAttributes: resultAttributes,
                geometry: featureGeometry,
                dirtyData: item
            });
        });

        self.emit("layer-identify-features", features);

        return features;
    }

    /**
     * Создание нового объекта и сохранение в dataSource
     * @param params
     * @param {object} params.template - шаблон свойств создаваемого объекта
     * @param {boolean} params.snap - привязка к другим объектам (true, false)
     */
    draw(params) {
        let self = this;

        let layerId = self.store.id;

        if (!params) params = {};

        params.layerId = layerId;
        params.dataSource = self.layer.getSource();
        params.fields = self.getFields();
        params.type = self.getGeometryType() || params.type;

        let draw = new Interaction.Draw(params);
        self.interactionManager.addInteraction(draw);
    }

    /**
     * Включает модификацию объектов слоя
     * @param params
     */
    modify(params) {
        let self = this;

        if (!params) params = {};
        params.source = self.layer.getSource();

        let modify = new Interaction.Modify(params);
        self.interactionManager.addInteraction(modify);
    }

    /**
     * Удаляет объекты слоя
     * @param params
     */
    //todo доделать синхронизацию между добавлением в dataSource и транзакцией
    remove(params) {
        let self = this;
        let feature = params.feature;
        let mode = params.mode;
        self.layer.getSource().removeFeature(feature);
    }

    /**
     * Включает или выключает привязку к другим объектам слоя
     * @param params
     */
    snap(params) {
        let self = this;

        if (!params) params = {};
        params.source = self.layer.getSource();

        let interaction = new Interaction.Snap(params);

        if (params.active) {
            self.map.addInteraction(interaction.interaction);
            self.interactions.snap = interaction;
        }
        if (!params.active) {
            self.map.removeInteraction(self.interactions.snap.interaction);
            self.interactions.snap = undefined;
        }
    }

    /**
     * Выполняет запрос к объектам слоя (через сервис или локально)
     * @param params
     */
    query(params) {
    }

    /**
     * Прокси для запроса легенды
     * Возвращает либо асинхронную, либо синхронную версию
     * @return {*}
     */
    legend() {
        let self = this;
        let geometryType = self.getGeometryType();

        let isStyleArray = self.store.styleArray;
        if (isStyleArray && geometryType && geometryType !== "anyGeometryType") {
            return self._legendSync();
        }

        if (geometryType === "anyGeometryType" || geometryType === null || geometryType === undefined) {
            return self._legendAsync();
        }

        return self._legendAsync();
    }

    //
    // Возвращает описание легенды слоя
    // @return {*}
    //
    async _legendAsync() {
        let self = this;
        let geometryType = self.getGeometryType();

        if (self.store.styleArray) {
            let styles = self.store.styleArray;
            styles = styles.map((item) => {
                item.geometryType = geometryType === "anyGeometryType" || geometryType === undefined ? item.geometryType : geometryType;
                let style = Object.assign({}, item.style);
                let symbolExample = new SymbolExample({geometryType: geometryType, style: style});
                item.symbolExample = symbolExample;
                return item;
            });

            self.emit("layer-legend", {
                legend: styles
            });

            return styles;
        }
        else {
            let dataSource = self.store.sourceFabric.sourceObject;
            await dataSource.loadSourceProperties();
            geometryType = self.getGeometryType();
            let extractedStyle = new ExtractStyle(self.layer.getStyle());

            let item = new LegendItem({
                geometryType: geometryType,
                properties: {},
                label: "любые значения",
                symbolExample: new SymbolExample({geometryType: geometryType, style: {}})
            });

            self.emit("layer-legend", {
                legend: [item]
            });

            return [item];
        }
    }

    //
    // Возвращает легенду слоя немендленно (синхронно).
    // Внимание, для избежания потери элементов легенды используйте данный метод только после вызова layer.setStyle(style)
    // @return {array(LegendItem)} - массив элементов легенды слоя
    //
    async _legendSync() {
        let self = this;
        let geometryType = self.getGeometryType();
        if (self.store.styleArray) {
            let styles = self.store.styleArray;
            styles = styles.map((item) => {
                item.geometryType = geometryType === "anyGeometryType" || geometryType === undefined ? item.geometryType : geometryType;
                let style = Object.assign({}, item.style);
                let symbolExample = new SymbolExample({geometryType: geometryType, style: style});
                item.symbolExample = symbolExample;
                return item;
            });

            self.emit("layer-legend", {
                legend: styles
            });

            return styles;
        }
        else {
            geometryType = self.store.params.geometryType;
            let item = new LegendItem({
                geometryType: geometryType,
                properties: {},
                label: "любые значения",
                symbolExample: new SymbolExample({geometryType: geometryType, style: {}})
            });

            self.emit("layer-legend", {
                legend: [item]
            });

            return [item];
        }
    }

    /**
     * Возвращает тип геометрии слоя
     * @return {string} - тип геометрии
     */
    getGeometryType() {
        let self = this;
        let geometryType;
        try {
            geometryType = self.store.sourceFabric.sourceObject.getGeometryType() || self.store.geometryType();
        }
        catch (e) {
            geometryType = undefined;
        }
        return geometryType;
    }

    /**
     * Возвращает описание полей слоя
     * @return {*|{getArray: (function()), getObject: (function())}}
     */
    getFields() {
        return this.store.sourceFabric.sourceObject.getFields();
    }

    /**
     * @param style - объект типа style
     */
    setStyle(style) {
        let self = this;
        self.layer.setStyle(style.getStyle());
        self.store.styleArray = style.legend();

        self.emit("layer-change-style", {
            layerId: self.store.id,
            style: style
        });
    }

    /**
     * Получает стиль объекта (слоя)
     */
    getStyle() {
        let self = this;
        let style = self.store.style;
        if (!style) {
            this.initDefaultStyle();
        }
        return this.layer.getStyle();
    }

    initDefaultStyle() {
        let self = this;
        try {
            let geometryType = self.getGeometryType();
            if (geometryType) {
                self.setStyle(DefaultStyle[geometryType]);
            }
            else {
                let dataSource = self.store.sourceFabric.sourceObject;
                dataSource
                    .loadSourceProperties()
                    .then((data) => {
                        let geometryType = dataSource.getGeometryType();
                        self.store.geometryType = geometryType;
                        //self.setStyle(DefaultStyle[geometryType]);
                    })
            }

        }
        catch (e) {

        }
    }

    /**
     * Выполняет поисковый запрос к слою
     * @param {object} params
     * @param {string} params.query - поисковый запрос, например, ПС-12
     * @param {number} params.minLength - минимальная длина запроса, по умолчанию - 1 символ
     * @returns {*}
     */
    search(params) {
        let self = this;
        let query = params.query;
        let temporaryIdField = "__id__";
        let minQueryLength = params.minLength === undefined ? 1 : params.minLength;
        let maxResultCount = params.maxResultCount === undefined ? 5 : params.maxResultCount;
        if (query.length < minQueryLength) {
            return [];
        }
        let layer = this.layer;
        let store = this.store;
        let source = layer.getSource();
        let features = self.getFeatures().map(function (item) {
            let properties = item.getProperties();
            delete properties.geometry;
            properties[temporaryIdField] = item.getId();
            return properties;
        });
        let keys = features.length > 0 ? Object.keys(features[0]) : [];
        keys = keys.filter((field) => {
            return field !== "geometry" && field !== temporaryIdField;
        });
        delete keys.geometry;
        let task = new fuse(features, {
            caseSensitive: true,
            includeScore: true,
            shouldSort: true,
            threshold: 0.2,
            minMatchCharLength: 1,
            keys: keys
        });
        let result = task.search(query);

        let displayField = self.getDisplayField();
        result = result.map(function (element) {
            let geometry = source.getFeatureById(element.item[temporaryIdField]).getGeometry();
            delete element.item[temporaryIdField];
            let resultAttributes = generateField(element.item, store);
            let feature = new ol.Feature(element.item);

            return new searchResult({
                layerId: store.id,
                layerName: store.alias,
                queryGeometry: geometry,
                attributes: element.item,
                preparedAttributes: resultAttributes,
                geometry: geometry,
                dirtyData: feature,
                score: element.score,
                title: feature.get(displayField) || ""
            })
        });
        result = result.filter(function (item, index) {
            return index < maxResultCount;
        });

        self.emit("layer-search-complete", result);

        return result;
    }

    /**
     * Динамическое изменение на режим отображения heatmap
     */
    setMode(params) {
        let self = this;
        let mode = params.mode || "vectorMode";
        let currentMode = self.store.mode || "vectorMode";
        if (mode === "vectorMode" && currentMode === "vectorMode") return;
        if (mode === "heatMode" && currentMode === "vectorMode") {
            let source = self.layer.getSource();
            params.source = source;
            let layer = new ol.layer.Heatmap(params);
            layer.setProperties({
                id: self.store.id,
                className: self.store.className
            });
            self.layer = layer;
            let map = self.map;
            let mapLayers = map.getLayers().getArray();
            let targetLayer = mapLayers.find((item) => {
                return item.get("id") === self.store.id;
            });
            map.removeLayer(targetLayer);
            map.addLayer(layer);
            self.store.mode = mode;
        }
        if (mode === "vectorMode" && currentMode === "heatMode") {
            let source = self.layer.getSource();
            let createParams = self.store.params;
            let layer = createLayer(createParams);
            layer.setProperties({
                id: self.store.id,
                className: self.store.className
            });
            self.layer = layer;
            let map = self.map;
            let mapLayers = map.getLayers().getArray();
            let targetLayer = mapLayers.find((item) => {
                return item.get("id") === self.store.id;
            });
            map.removeLayer(targetLayer);
            map.addLayer(layer);
            self.store.mode = mode;
        }
    }

    /**
     * Управление режимом применения изменений при редактировании
     * @param params {object}
     * @param params.transactionMode {string} - Возможные значения: direct - выполнение транзакций немедленно, stack - передача транзакий в глобальный стек (сессии)
     */
    setTransactionMode(params) {
        let self = this;
        self.store.transactionMode = params.transactionMode;
        self.emit("layer-transaction-mode-changed", {
            layerId: self.store.id,
            mode: params.transactionMode
        });
    }

    /**
     * Возвращает текущий режим применения транзакций редактирования
     * @returns {*|string}
     */
    getTransactionMode() {
        return this.store.transactionMode || "stack";
    }

    /**
     * Создание транзакции, которая в зависимости от текущего значения режима - getTransactionMode() - выполняется немедленно или передается в стек операций
     */
    async transact(params) {
        let self = this;
        let transactionMode = self.getTransactionMode();
        let sourceDescriptor = self.store.sourceFabric.getDescriptor();

        switch (transactionMode) {
            case "direct":
                return sourceDescriptor.sourceObject.transaction(params);
            case "stack":
                return sourceDescriptor.sourceObject.toStack(params);
            default:
                return sourceDescriptor.sourceObject.transaction(params);
        }
    }

    /**
     * Применяет переданные свойства к слою
     * @param {object} params - изменеяемые свойства
     * @param params.visible {boolean} - видимость слоя
     * @param params.opacity {number} - прозрачносиь слоя
     * @param params.minResolution {number} - минимальное разрешение
     * @param params.maxResolution {number} - максимальное разрешение
     * @param params.style {object} - конфигурация стиля
     * @param params.zIndex {number} - уровень слоя
     * @return {boolean} - результат выполнения операции
     */
    setOptions(params) {
        let self = this;
        let layer = self.layer;

        let result = true;
        try {
            params.visible && layer.setVisible(params.visible);
            params.opacity && layer.setOpacity(params.opacity);
            params.minResolution && layer.setMinResolution(params.minResolution);
            params.maxResolution && layer.setMaxResolution(params.maxResolution);
            params.zIndex && layer.setZIndex(params.zIndex);
            params.map && layer.setMap(params.map);
            params.style && self.setStyle(params.style);
            if (params.interactionManager) {
                self.interactionManager = params.interactionManager;
            }
        }
        catch (e) {
            result = false;
        }
        return result;
    }

    /**
     * Устанавливает прозрачность слоя
     * @param {number} value - степень прозначности в диапазоне от 0 до 1
     */
    setOpacity(value) {
        let self = this;
        let layer = self.layer;
        let opacity = (value >= 0.0 && value <= 1.0) ? value : 1;
        layer.setOpacity(opacity);
    }

    /**
     * Возвращает прозрачность слоя
     */
    getOpacity() {
        return this.layer.getOpacity();
    }

    getSourceObject() {
        return this.store.sourceFabric.sourceObject;
    }

    /**
     * Возвращает имя поля, используемое для подписи
     * @return {*}
     */
    getDisplayField() {
        return this.store.displayField;
    }

    /**
     * Получить список объектов источника
     * @param params
     * @return {*|Array.<ol.Feature|ol.render.Feature>|ol.Collection.<ol.Feature>|Array.<ol.Feature>}
     */
    getFeatures(params) {
        return this.getSourceObject().getFeatures(params);
    }

    /**
     * Получение feature из сервиса
     * @param featureId {string} - featureId
     */
    async loadFeatureFromService(featureId) {
        let self = this;
        let featureData = await self.getSourceObject().loadFeatureFromService(featureId);
        return featureData;
    }

    /**
     * Добавить объекты в источник
     * @param data
     */
    addFeatures(data) {
        this.getSourceObject().addFeatures(data);
    }

    /**
     * Удаление feature из источника
     * @param feature
     */
    removeFeature(feature) {
        this.getSourceObject().removeFeature(feature)
    }

    /**
     * Удаление feature из источника через указания свойства и значения
     * @param {object} params - параметры поиска
     * @param params.key {string} - имя свойства
     * @param params.value {string} - искомое значение
     */
    removeFeatureByProperty(params) {
        this.getSourceObject().removeFeatureByProperty(params);
    }

    /**
     * Подписка на события
     * @param eventName
     * @param listener
     */
    on(eventName, listener) {
        let self = this;
        let eventStore = self.events;
        eventStore[eventName] = listener;
    }

    /**
     * Выключение подписки на событие
     * @param eventName
     */
    off(eventName) {
        let self = this;
        let eventStore = self.events;
        if (eventStore[eventName]) delete eventStore[eventName];
    }

    /**
     * Инициация события с контекстом
     * @param eventName
     * @param context
     */
    emit(eventName, context) {
        let self = this;
        let eventStore = self.events;
        let listener = eventStore[eventName];
        if (listener && typeof listener === "function") {
            listener(context);
        }
    }
}

function createLayer(params) {
    //создание векторного соля
    let layer = new ol.layer.Vector({
        source: params.sourceFabric.getSource(),
        visible: params.visible || true,
        opacity: params.opacity || 1,
        minResolution: params.minResolution || 0,
        maxResolution: params.maxResolution || 0,
        renderBuffer: params.renderBuffer || 10,
        style: params.style || undefined,
        updateWhileAnimating: params.updateWhileAnimating || false,
        updateWhileInteracting: params.updateWhileInteracting || false
    });

    layer.setProperties({
        id: params.id,
        className: params.className
    });

    return layer;
}

function createSourceFabric(params) {
    let sourceFabric = new DataSource(params);
    return sourceFabric;
}

function generateEmptyMapping(name, value) {
    let mapping = {
        name: name,
        alias: name,
        group: "none",
        value: value
    };
    return mapping;
}

function generateField(featureAttributes, store) {
    let resultAttributes = [];
    for (let key in featureAttributes) {
        let localMapping = store.mappings[key] === undefined ? generateEmptyMapping(key, featureAttributes[key]) : store.mappings[key];
        resultAttributes.push({
            name: key,
            alias: localMapping.alias,
            group: localMapping.group,
            value: featureAttributes[key]
        });
    }
    return resultAttributes;
}

function testDisplayField(value) {
    if (typeof value === "string" && value !== "") return value;
    return "aliasName";
}

module.exports = VectorLayer;