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;