Source: core/map.js

let ol = require('openlayers');

let AbstractMap = require('./abstractMap');
let baseMapLayer = require('./../../modules/layers/baseMap');
let ExtentResult = require('./extentResult');

let GraphicLayer = require('./../layers/graphic');
let DefaultSelectionStyle = require('./../styles/defaultSelectionStyle');

let Interaction = require('./../interactions/interaction');
let TransactionStack = require('./../editing/transactionStack');

let InteractionManager = require('./../interactions/interactionManager');

let Exporters = require('./../io/export/exportFabric');

let GeocoderFabric = require('./../geocoders/geocoderFabric');

/**
 * Класс Map предназначен для создания плоских, самых обыкновенных карт
 * @extends AbstractMap
 */
class Map extends AbstractMap {
    /**
     * Создает объект карты в соответствии с переданными параметрами (params)
     * @param {object} params - начальные параметры для создания экземпляра карты
     * @param params.name {string} - имя карты
     * @param params.target {string} - идентификатор DOM элемента
     * @param params.layers {array} - массив слоев, включаемых в карту при начале работы
     * @param params.loadTilesWhileAnimating {boolean} - загружать тайлы при анимации
     * @param params.loadTilesWhileInteracting {boolean} - загружать тайлы при вызове взаимодействий
     * @param params.longitude {number} - стартовая долгота
     * @param params.latitude {number} - стартовая долгота
     * @param params.zoom {number} - уровень приближения
     * @param params.projection {string} - код проекции
     * @param params.renderer {string} - код рендерера - webgl, canvas (по умолчанию - webgl)
     * @param params.baseMap {string} - код базовой карты
     * @param params.snapDefault {boolean} - включать привязку к объектам слоев при их добавлении на карту
     *
     */
    constructor(params) {
        super(params);

        this.map = this._create(params);

        this.initEvents(params);
        this.initStore(params);
        this.setBaseMap(params.baseMap);
        this.initStack(params);
        this.initGraphics(params);

        this.emit("ready", {
            map: this.getDescriptor()
        });

        this.emit("load", {
            store: this.store,
            map: this.map
        })
    }

    /**
     * Выполняет центрирование карты по заданным координатам
     * @param params
     * @param params.coordinates {array} - координаты центра
     * @param params.transform {object} - необязательный параметр трансформации
     * @param params.transform.source {string} - исходная проекция
     * @param params.transform.destination {string} - целевая проекция
     */
    center(params) {
        let map = this.map;
        let view = map.getView();
        let coordinates = params.coordinates;
        let transform = params.transform;
        let center = transform === undefined ? ol.proj.fromLonLat([coordinates[0], coordinates[1]]) : ol.proj.transform(coordinates, transform.source, transform.destination);
        view.setCenter(center);

        self.emit("extent-change", self.getExtent());
    }

    /**
     * Устанавливает экстент карты
     * @param params
     * @param params.extent {object} - целевой экстент
     * @param params.geometry {object} - целевая геометрия
     * @param params.nearest {boolean} - флаг принудительного вписывания экстента к экстенту/геометрии
     */
    setExtent(params) {
        let self = this;
        let map = this.map;
        let view = map.getView();
        let extent = params.extent || params.geometry;
        let nearest = params.nearest || false;
        view.fit(extent, {nearest: nearest});

        self.emit("extent-change", self.getExtent());
    }

    /**
     * Возвращает дескрипорт карты (ol.Map)
     * @return {Map}
     */
    getDescriptor() {
        return this;
    }

    /**
     * Осуществление привязки событий
     * @param params
     * @param params.identify {object} - описание привязки события идентификации
     */
    initEvents(params) {
        this.events = {};
        if (typeof params.identify === 'object') {
            this.events.identify = {
                eventName: params.identify.eventName
            };
        }
    }

    /**
     * Получение экстента
     * @param func - функция обратного вызова
     * @return {Promise} extent - Экстент карты, приведенный к типу ExtentResult
     */
    extent(func) {
        let self = this;
        return new Promise(function (resolve) {
            resolve(self.map.on("moveend", function (evt) {
                let extent = self.getExtent();
                let extentResult = new ExtentResult(extent);

                func(extentResult);
                self.emit("extent-calculated", extentResult);
            }));
        });
    }

    /**
     * Инициализация хранилища по заданным параметрам
     * @param {object} params - опции создания карты
     * @param {string} params.GUID - идентификатор карты
     * @return {Map}
     */
    initStore(params) {
        let self = this;
        let map = self.map;

        self.store = {
            id: params.id || params.GUID,
            layers: {},
            overlays: [],
            params: params,
            baseMap: {
                id: null,
                layers: [],
                config: new baseMapLayer({id: "basemap.default"})
            },
            snapDefault: params.snapDefault || true
        };

        self.interactions = {};

        self.interactionManager = new InteractionManager({map: map});

        self.emit("store-init", self.store);

        return this;
    }

    /**
     * Инициализация графикческого слоя
     * @param {object} params - опции создания графического слоя
     * @return {GraphicLayer}
     */
    initGraphics(params) {
        let self = this;
        let map = self.map;

        let graphics = new GraphicLayer({id: "graphics", className: "graphics", url: "localhost"});
        map.addLayer(graphics.layer);

        self.store.graphics = graphics;
        return graphics;
    }

    /**
     * Подсветка объекта на карте
     * @param {object} params - опции подсветки
     * @param params.clear {boolean} - удалить предыдущие объекты выделения, по умолчанию выключено
     * @param params.feature {ol.Feature} - объект выделения
     * @param params.geometry {ol.geom} - геометрия выделения
     */
    highlight(params) {
        let self = this;
        let graphics = self.store.graphics;
        if (params.clear && params.clear === true) {
            self.clearGraphics();
        }

        if (params.feature) {
            let geometryType = params.feature.getGeometry().getType();
            let style = DefaultSelectionStyle[geometryType];
            graphics.setStyle(style);
            return graphics.add({feature: params.feature});
        }

        if (params.geometry) {
            let geometryType = params.geometry.getType();
            let style = DefaultSelectionStyle[geometryType];
            graphics.setStyle(style);
            return graphics.addGeometry({geometry: params.geometry});
        }
    }

    /**
     * Очистка графики на карте
     */
    clearGraphics() {
        let self = this;
        let graphics = self.store.graphics;
        graphics.clear();
    }

    /**
     * Инициализация стека, ассоциированного с картой (п.с. - в дальнейшем будет создаваться локальный стек для каждого нового экземпляра карты)
     * @return {Map}
     */
    initStack() {
        this.transactionStack = TransactionStack;
        return this;
    }

    /**
     * Подписка на событие клика по карте
     * @param {function} func - функция обратного вызова
     * @return {Promise}
     */
    click(func) {
        let self = this;
        return new Promise(function (resolve) {
            resolve(self.map.on("click", function (evt) {
                func(evt);

                self.emit("click", evt);
            }));
        });
    }

    /**
     * Добавляет слои на карту
     * @param data {array | object} - массив слоев любого допустимого типа
     * @return {Map}
     */
    add(data) {
        let self = this;
        let map = self.map;
        let store = self.store;
        if (Array.isArray(data)) {
            data.forEach(function (item) {
                if (!store.layers[item.store.id]) {
                    store.layers[item.store.id] = item;
                    map.addLayer(item.layer);
                    item.setMap(map);
                    item.setOptions({interactionManager: self.interactionManager});

                    /**
                     * @event Map#layer-add-result
                     * @return {Layer} - добавленный слой
                     */
                    self.emit("layer-add-result", item);
                }
            });

            /**
             * @event Map#layers-add-result
             * @return {array} - добавленные слой
             */
            self.emit("layers-add-result", data);
        }
        else {
            let layerId = data.store.id;
            if (!store.layers[layerId]) {
                store.layers[layerId] = data;
                map.addLayer(data.layer);
                data.setMap(map);
                data.setOptions({interactionManager: self.interactionManager});

                self.emit("layer-add-result");
            }

        }

        /**
         * Включать привязку для добавленных слоев
         */
        if (self.store.snapDefault) self.snap({all: true});
        return self;
    }

    addOverlay(data) {
        let self = this;
        let overlay = data.getOverlay();
        let map = self.map;
        let store = self.store;
        map.addOverlay(overlay);
        store.overlays.push(overlay);
    }

    removeOverlay(id) {
        let self = this;
        let store = self.store;
        let overlay = store.overlays.find((item) => {
            return item.id === id;
        });
        if (overlay) {
            self.map.removeOverlay(overlay);
            self.store.overlays = self.store.overlays.filter((item) => {
                return item.id !== id;
            })
        }
    }

    /**
     * Удаляет слой с карты
     * @param layerId
     * @return {Map}
     */
    remove(layerId) {
        let self = this;
        let storeLayer = self.getLayerById(layerId);
        self.map.removeLayer(storeLayer.layer);
        delete self.store.layers[layerId];

        self.emit("layer-remove", layerId);
        return this;
    }

    /**
     * Инструмент выделения объектов на карте
     * @param {object} params
     */
    select(params) {
        let self = this;

        if (!params) params = {};
        params.store = self.store;

        let select = new Interaction.Select(params);
        self.interactionManager.addInteraction(select);
    }

    snap(params) {
        let self = this;
        let layers = self.getLayers();
        let mode = params.all || true;
        let ids = params.ids;
        if (mode === true) {
            layers.forEach((item) => {
                item.snap({active: true})
            });
        }
        if (mode !== true && ids) {
            layers.forEach((item) => {
                item.snap({active: false})
            });
            ids.forEach((id) => {
                let layer = self.getLayerById(id);
                layer.snap({active: true});
            })
        }
    }

    /**
     * Завершает все взаимодействия
     */
    stopInteractions() {
        this.interactionManager.removeInteractions();
    }

    /**
     * Возвращает экстент карты
     * @return {ol.Extent} - экстент карты
     */
    getExtent() {
        return this.map.getView().calculateExtent();
    }

    /**
     * @return {array} - массив объектов типа opengis.api.layers.vector
     */
    getLayers() {
        let store = this.store;
        let layerIds = Object.keys(store.layers);
        if (layerIds === undefined) return [];
        return layerIds.map(function (item) {
            return store.layers[item];
        });
    }

    /**
     * @param id -  идентификатор слоя
     * @return {*}
     */
    getLayerById(id) {
        let store = this.store;
        let targetLayer;
        try {
            targetLayer = store.layers[id];
        }
        catch (e) {
            targetLayer = undefined;
        }
        return targetLayer;
    }

    /**
     * Возвращает слой по имени класса
     * @param className
     * @returns {*}
     */
    getLayerByClassName(className) {
        let self = this;
        let store = self.store;
        let targetLayer;
        try {
            targetLayer = self.getLayers().find((item) => {
                return item.store.className === className;
            })
        }
        catch (e) {
            targetLayer = undefined;
        }
        return targetLayer;
    }

    /**
     * @param ids -  идентификаторы слоя
     * @return {*}
     */
    getLayerByIds(ids) {
        let filterLayers = [];
        let layers = this.store.layers;
        for (let item in layers) {
            for (let i = 0; i < ids.length; i++) {
                if (item === ids[i]) {
                    filterLayers.push(layers[item]);
                    break;
                }
            }
        }
        return filterLayers;
    }

    /**
     * возвращает идентификатор текущей базовой карты
     * @return {null}
     */
    getBaseMapId() {
        return this.store.baseMap.id;
    }

    /**
     * возвращает массив встроенных базовых карт
     * @return {*}
     */
    getBaseMapList() {
        return this.store.baseMap.config.getBaseMapEmbeddedList();
    }

    /**
     * устанавливает базовую карту по идентификатору
     * @param id
     */
    setBaseMap(id) {
        let self = this;
        let map = self.map;
        let store = self.store;
        let currentBaseMaps = store.baseMap.layers;
        currentBaseMaps.forEach(function (item) {
            map.removeLayer(item);
        });
        id = id === undefined ? "undefined" : id;
        let subLayers = store.baseMap.config.getBaseMapById(id)["layers"];
        let mapLayers = map.getLayers();
        subLayers.forEach(function (item) {
            mapLayers.insertAt(0, item);
            store.baseMap.layers.push(item);
        });
        store.baseMap.id = id;
        self.emit("basemap-change", {
            id: id, //идентификатор базовой карты
            layers: subLayers //слои базовой карты
        });
    }

    /**
     * @param params
     * @param params.func - функция обратного вызова
     * @param params.layers - перечень слоев для идентификации
     * @returns {*}
     */
    identify(params) {
        let self = this;
        let func = params.func;
        let identifyConfiguration = self.events.identify;
        if (identifyConfiguration) {
            let eventName = identifyConfiguration.eventName;
            return new Promise(function (resolve) {
                resolve(self.map.on(eventName, async function (evt) {
                    let layers = self.getLayers();
                    let requests = layers.map(function (item) {
                        return item.identify(evt);
                    });
                    let data = await Promise.all(requests);
                    let features = [];
                    for (let i = 0; i < data.length; i++) {
                        features = features.concat(data[i]);
                    }
                    features = features.map(function (item, index) {
                        item.index = index;
                        return item;
                    });
                    func(features);
                    self.emit("identify-features", features);
                }));
            });
        }
        else {
            return new Promise(function (resolve) {
                self.emit("identify-features", []);
                resolve([]);
            })
        }
    }

    /**
     * @param params {object} - опции поиска
     * @param params.query {string} - Строка запроса, содержащая часть или полное значение поля
     * @param params.includeGeocoding {boolean} - использовать результаты геокодирвоания (по умолчанию - true)
     * @return {Promise}
     */
    search(params) {
        let self = this;

        return new Promise(async function (resolve) {
            let layers = self.getLayers();
            let requests = layers.map(function (item) {
                return item.search(params);
            });

            let includeGeocoding = params.includeGeocoding || true;
            let geocoderResult = self.geocode({location: params.query});
            if (includeGeocoding) requests.push(geocoderResult);

            let data = await Promise.all(requests);
            let features = [];

            for (let i = 0; i < data.length; i++) {
                features = features.concat(data[i]);
            }

            features = features.map(function (item, index) {
                item.index = index;
                return item;
            });

            self.emit("search-complete", features);

            resolve(features);
        })
    }

    /**
     * @param params {object} - опции геокодирования
     * @param params.location {string} - поисковый запрос
     * @return {*|Promise}
     */
    geocode(params) {
        let self = this;

        let geocoder = this.store.geocoder ? this.store.geocoder : (new GeocoderFabric({geocoderType: "yandex"})).getGeocoder();
        this.geocoder = geocoder;

        let location = params.location;
        return geocoder
            .geocode(location)
            .then((data) => {
                let items = data.map((item) => {
                    return item.getSearchResult();
                });
                return new Promise((resolve) => {
                    self.emit("geocode-complete", items);
                    resolve(items);
                });
            });
    }

    /**
     * Выполняет сохранение карты в файл, по умолчанию с расширением png (map.png)
     * @param params {object} - опции эскпорта
     * @param params.fileExtention {string} - расширение файла
     * @param params.fileName {string} - имя файла
     * @return {Map}
     */
    saveAs(params) {
        let fileExtention = params.fileExtention || "png";
        let fileName = params.fileName || "map";

        let saveMethods = {
            "png": Exporters.PngExporter,
            undefined: Exporters.PngExporter
        };

        let concreteExporter = new saveMethods[fileExtention]({name: fileName, map: this.map});
        concreteExporter.saveAs();
        return this;
    }

    /**
     * Возвращает легенду для всех слоев
     * @return {Promise.<Array>}
     */
    async legend() {
        let result = [];
        let layers = this.getLayers();
        for (let i = 0; i < layers.length; i++) {
            try {
                let item = {
                    index: i,
                    layerId: layers[i].store.id,
                    legend: await layers[i].legend()
                };
                result.push(item);
            }
            catch (e) {

            }
        }
        return result;
    }

    /**
     * Возвращает легенду для всех слоев синхронно
     * @return {Array}
     */
    legendSync() {
        let result = [];
        let layers = this.getLayers();
        for (let i = 0; i < layers.length; i++) {
            try {
                let item = {
                    index: i,
                    layerId: layers[i].store.id,
                    legend: layers[i]._legendSync()
                };
                result.push(item);
            }
            catch (e) {

            }
        }
        return result;
    }

    on(eventName, listener) {
        let self = this;
        let eventStore = self.events;
        eventStore[eventName] = listener;
    }

    off(eventName) {
        let self = this;
        let eventStore = self.events;
        if (eventStore[eventName]) delete eventStore[eventName];
    }

    emit(eventName, context) {
        let self = this;
        let eventStore = self.events;
        let listener = eventStore[eventName];
        if (listener && typeof listener === "function") {
            listener(context);
        }
    }

    /**
     * @private
     * @param params {object} - параметры создания экземпляра карты
     * @param params.srs {string} - код системы координат
     * @param params.projection {string} - код системы координат
     * @param params.layers {array} - коллекция слоев
     * @param params.controls {array} - коллекция контролов
     * @param params.zoom {number} - масштабный уровень
     * @param params.renderer {string} - тип графического движка (canvas/webgl)
     * @param params.loadTilesWhileAnimating {boolean} - загружать тайлы во время анимации
     * @param params.loadTilesWhileInteracting {boolean} - загружать тайлы во время работы взаимодействий
     * @param params.longitude {number} - долгота
     * @param params.latitude {number} - широта
     * @return {ol.Map}
     */
    _create(params) {
        params = params || {};

        let elementName = testMapElement(params);

        return new ol.Map({
            target: elementName,
            layers: params.layers || [],
            controls: params.controls || [],
            loadTilesWhileAnimating: params.loadTilesWhileAnimating || true,
            loadTilesWhileInteracting: params.loadTilesWhileInteracting || true,
            view: new ol.View({
                center: ol.proj.fromLonLat([params.longitude || 0, params.latitude || 0]),
                zoom: params.zoom || 12,
                projection: params.srs || params.projection || 'EPSG:3857'
            }),
            renderer: params.renderer || "webgl"
        });
    }

    addControl(control) {

    }

    removeControl(control) {

    }
}

function testMapElement(parameters) {
    let elementName = parameters.elementName || `map-auto`;
    let element = document.getElementById(elementName);
    if (!element) {
        document.body.innerHTML += `<div id='${elementName}'></div>`;
    }
    return elementName;
}

module.exports = Map;