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;