UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

1,416 lines (1,243 loc) • 57 kB
"use strict"; var noop = require("../../core/utils/common").noop, extend = require("../../core/utils/extend").extend, each = require("../../core/utils/iterator").each, _Number = Number, _String = String, _abs = Math.abs, _round = Math.round, _min = Math.min, _max = Math.max, _sqrt = Math.sqrt, DataHelperMixin = require("../../data_helper"), _isFunction = require("../../core/utils/type").isFunction, _isArray = Array.isArray, vizUtils = require("../core/utils"), _parseScalar = vizUtils.parseScalar, _patchFontOptions = vizUtils.patchFontOptions, _normalizeEnum = vizUtils.normalizeEnum, _noop = noop, _extend = extend, _each = each, _concat = Array.prototype.concat, TYPE_AREA = "area", TYPE_LINE = "line", TYPE_MARKER = "marker", STATE_DEFAULT = 0, STATE_HOVERED = 1, STATE_SELECTED = 2, STATE_TO_INDEX = [0, 1, 2, 2], TOLERANCE = 1, SELECTIONS = { "none": null, "single": -1, "multiple": NaN }; function getSelection(selectionMode) { var selection = _normalizeEnum(selectionMode); selection = selection in SELECTIONS ? SELECTIONS[selection] : SELECTIONS.single; if (selection !== null) { selection = { state: {}, single: selection }; } return selection; } function EmptySource() {} EmptySource.prototype.count = function () { return 0; }; function ArraySource(raw) { this.raw = raw; } ArraySource.prototype = { constructor: ArraySource, count: function count() { return this.raw.length; }, item: function item(index) { return this.raw[index]; }, geometry: function geometry(item) { return { coordinates: item.coordinates }; }, attributes: function attributes(item) { return item.attributes; } }; function GeoJsonSource(raw) { this.raw = raw; } GeoJsonSource.prototype = { constructor: GeoJsonSource, count: function count() { return this.raw.features.length; }, item: function item(index) { return this.raw.features[index]; }, geometry: function geometry(item) { return item.geometry; }, attributes: function attributes(item) { return item.properties; } }; function isGeoJsonObject(obj) { return _isArray(obj.features); } // The problem is that when remote source returns an object (not an array) the data.DataSource internally wraps it into array (of one element) // So specific `if` clause is required to recognize GeoJson object in the returned `items` function unwrapFromDataSource(source) { var sourceType; if (source) { if (isGeoJsonObject(source)) { sourceType = GeoJsonSource; } else if (source.length === 1 && source[0] && isGeoJsonObject(source[0])) { sourceType = GeoJsonSource; source = source[0]; } else if (_isArray(source)) { sourceType = ArraySource; } } sourceType = sourceType || EmptySource; return new sourceType(source); } // The first problem is that when our DataSource is updated with an object (not an array) it considers such object a bunch of options. // So single object has to be wrapped into array in order to be passed to the data.DataSource as is. // The second problem is that when our DataSource is updated with `null` or `undefined` it does nothing - callback is not triggered (it is because of charts). // So `null` or `undefined` is changed to empty array. function wrapToDataSource(option) { return option ? isGeoJsonObject(option) ? [option] : option : []; } function customizeHandles(proxies, callback, widget) { callback.call(widget, proxies); } // DEPRECATED_15_2 function customizeHandles_deprecated(proxies, callback) { var i, ii = proxies.length, proxy, settings; for (i = 0; i < ii; ++i) { proxy = proxies[i]; settings = callback.call(proxy, proxy) || {}; proxy.applySettings(settings); if (settings.isSelected) { proxy.selected(true); } } } // DEPRECATED_15_2 function patchProxies(handles, name, data) { var type = { areas: "area", markers: "marker" }[name], i, ii = handles.length, dataItem; for (i = 0; i < ii; ++i) { handles[i].proxy.type = type; } if (type === "marker") { for (i = 0; i < ii; ++i) { dataItem = data.item(i); _extend(handles[i].proxy, { text: dataItem.text, value: dataItem.value, values: dataItem.values, url: dataItem.url }); } } } // TODO: Consider moving it inside a strategy function setAreaLabelVisibility(label) { label.text.attr({ visibility: label.size[0] / label.spaceSize[0] < TOLERANCE && label.size[1] / label.spaceSize[1] < TOLERANCE ? null : "hidden" }); } // TODO: Consider moving it inside a strategy function setLineLabelVisibility(label) { label.text.attr({ visibility: label.size[0] / label.spaceSize[0] < TOLERANCE || label.size[1] / label.spaceSize[1] < TOLERANCE ? null : "hidden" }); } // DEPRECATED_15_2 (just proxy.attribute(dataField) must stay) function getDataValue(proxy, dataField, deprecatedField) { return proxy.attribute(dataField) || proxy[deprecatedField]; } var TYPE_TO_TYPE_MAP = { Point: TYPE_MARKER, MultiPoint: TYPE_LINE, LineString: TYPE_LINE, MultiLineString: TYPE_LINE, Polygon: TYPE_AREA, MultiPolygon: TYPE_AREA }; function pick(a, b) { return a !== undefined ? a : b; } function guessTypeByData(sample) { var type = TYPE_TO_TYPE_MAP[sample.type], coordinates = sample.coordinates; if (!type) { if (typeof coordinates[0] === "number") { type = TYPE_MARKER; } else if (typeof coordinates[0][0] === "number") { type = TYPE_LINE; } else { type = TYPE_AREA; } } return type; } var selectStrategy = function selectStrategy(options, data) { var type = _normalizeEnum(options.type), elementType = _normalizeEnum(options.elementType), sample, strategy = _extend({}, emptyStrategy); if (data.count() > 0) { sample = data.geometry(data.item(0)); type = strategiesByType[type] ? type : guessTypeByData(sample); _extend(strategy, strategiesByType[type]); strategy.fullType = strategy.type = type; if (strategiesByGeometry[type]) { _extend(strategy, strategiesByGeometry[type](sample)); } if (strategiesByElementType[type]) { elementType = strategiesByElementType[type][elementType] ? elementType : strategiesByElementType[type]._default; _extend(strategy, strategiesByElementType[type][elementType]); strategy.elementType = elementType; strategy.fullType += ":" + elementType; } } return strategy; }; function applyElementState(figure, styles, state, field) { figure[field].attr(styles[field][state]); } var emptyStrategy = { setup: _noop, reset: _noop, arrange: _noop, updateGrouping: _noop }; var strategiesByType = {}; strategiesByType[TYPE_AREA] = { projectLabel: projectAreaLabel, transform: transformPointList, transformLabel: transformAreaLabel, draw: function draw(context, figure, data) { figure.root = context.renderer.path([], "area").data(context.dataKey, data); }, refresh: _noop, getLabelOffset: function getLabelOffset(label) { setAreaLabelVisibility(label); return [0, 0]; }, getStyles: function getStyles(settings) { var color = settings.color || null, borderColor = settings.borderColor || null, borderWidth = pick(settings.borderWidth, null), opacity = pick(settings.opacity, null); return { root: [{ "class": "dxm-area", stroke: borderColor, "stroke-width": borderWidth, fill: color, opacity: opacity }, { "class": "dxm-area dxm-area-hovered", stroke: settings.hoveredBorderColor || borderColor, "stroke-width": pick(settings.hoveredBorderWidth, borderWidth), fill: settings.hoveredColor || color, opacity: pick(settings.hoveredOpacity, opacity) }, { "class": "dxm-area dxm-area-selected", stroke: settings.selectedBorderColor || borderColor, "stroke-width": pick(settings.selectedBorderWidth, borderWidth), fill: settings.selectedColor || color, opacity: pick(settings.selectedOpacity, opacity) }] }; }, setState: function setState(figure, styles, state) { applyElementState(figure, styles, state, "root"); }, hasLabelsGroup: true, updateGrouping: function updateGrouping(context) { groupByColor(context); } }; strategiesByType[TYPE_LINE] = { projectLabel: projectLineLabel, transform: transformPointList, transformLabel: transformLineLabel, draw: function draw(context, figure, data) { figure.root = context.renderer.path([], "line").data(context.dataKey, data); }, refresh: _noop, getLabelOffset: function getLabelOffset(label) { setLineLabelVisibility(label); return [0, 0]; }, getStyles: function getStyles(settings) { var color = settings.color || settings.borderColor || null, width = pick(settings.borderWidth, null), opacity = pick(settings.opacity, null); return { root: [{ "class": "dxm-line", stroke: color, "stroke-width": width, opacity: opacity }, { "class": "dxm-line dxm-line-hovered", stroke: settings.hoveredColor || settings.hoveredBorderColor || color, "stroke-width": pick(settings.hoveredBorderWidth, width), opacity: pick(settings.hoveredOpacity, opacity) }, { "class": "dxm-line dxm-line-selected", stroke: settings.selectedColor || settings.selectedBorderColor || color, "stroke-width": pick(settings.selectedBorderWidth, width), opacity: pick(settings.selectedOpacity, opacity) }] }; }, setState: function setState(figure, styles, state) { applyElementState(figure, styles, state, "root"); }, hasLabelsGroup: true, updateGrouping: function updateGrouping(context) { groupByColor(context); } }; strategiesByType[TYPE_MARKER] = { project: projectPoint, transform: transformPoint, draw: function draw(context, figure, data) { figure.root = context.renderer.g(); this._draw(context, figure, data); }, refresh: _noop, hasLabelsGroup: false, getLabelOffset: function getLabelOffset(label, settings) { return [_round((label.size[0] + _max(settings.size || 0, 0)) / 2) + 2, 0]; }, getStyles: function getStyles(settings) { var styles = { root: [{ "class": "dxm-marker" }, { "class": "dxm-marker dxm-marker-hovered" }, { "class": "dxm-marker dxm-marker-selected" }] }; this._getStyles(styles, settings); return styles; }, setState: function setState(figure, styles, state) { applyElementState(figure, styles, state, "root"); this._setState(figure, styles, state); }, updateGrouping: function updateGrouping(context) { groupByColor(context); groupBySize(context); } }; var strategiesByGeometry = {}; strategiesByGeometry[TYPE_AREA] = function (sample) { var coordinates = sample.coordinates; return { project: coordinates[0] && coordinates[0][0] && coordinates[0][0][0] && typeof coordinates[0][0][0][0] === "number" ? projectMultiPolygon : projectPolygon }; }; strategiesByGeometry[TYPE_LINE] = function (sample) { var coordinates = sample.coordinates; return { project: coordinates[0] && coordinates[0][0] && typeof coordinates[0][0][0] === "number" ? projectPolygon : projectLineString }; }; var strategiesByElementType = {}; strategiesByElementType[TYPE_MARKER] = { _default: "dot", dot: { setup: function setup(context) { context.filter = context.renderer.shadowFilter("-40%", "-40%", "180%", "200%", 0, 1, 1, "#000000", 0.2); }, reset: function reset(context) { context.filter.dispose(); context.filter = null; }, _draw: function _draw(ctx, figure, data) { figure.back = ctx.renderer.circle().sharp().data(ctx.dataKey, data).append(figure.root); figure.dot = ctx.renderer.circle().sharp().data(ctx.dataKey, data).append(figure.root); }, refresh: function refresh(ctx, figure, data, proxy, settings) { figure.dot.attr({ filter: settings.shadow ? ctx.filter.id : null }); }, _getStyles: function _getStyles(styles, style) { var size = style.size > 0 ? _Number(style.size) : 0, hoveredSize = size, selectedSize = size + (style.selectedStep > 0 ? _Number(style.selectedStep) : 0), hoveredBackSize = hoveredSize + (style.backStep > 0 ? _Number(style.backStep) : 0), selectedBackSize = selectedSize + (style.backStep > 0 ? _Number(style.backStep) : 0), color = style.color || null, borderColor = style.borderColor || null, borderWidth = pick(style.borderWidth, null), opacity = pick(style.opacity, null), backColor = style.backColor || null, backOpacity = pick(style.backOpacity, null); styles.dot = [{ r: size / 2, stroke: borderColor, "stroke-width": borderWidth, fill: color, opacity: opacity }, { r: hoveredSize / 2, stroke: style.hoveredBorderColor || borderColor, "stroke-width": pick(style.hoveredBorderWidth, borderWidth), fill: style.hoveredColor || color, opacity: pick(style.hoveredOpacity, opacity) }, { r: selectedSize / 2, stroke: style.selectedBorderColor || borderColor, "stroke-width": pick(style.selectedBorderWidth, borderWidth), fill: style.selectedColor || color, opacity: pick(style.selectedOpacity, opacity) }]; styles.back = [{ r: size / 2, stroke: "none", "stroke-width": 0, fill: backColor, opacity: backOpacity }, { r: hoveredBackSize / 2, stroke: "none", "stroke-width": 0, fill: backColor, opacity: backOpacity }, { r: selectedBackSize / 2, stroke: "none", "stroke-width": 0, fill: backColor, opacity: backOpacity }]; }, _setState: function _setState(figure, styles, state) { applyElementState(figure, styles, state, "dot"); applyElementState(figure, styles, state, "back"); } }, bubble: { _draw: function _draw(ctx, figure, data) { figure.bubble = ctx.renderer.circle().sharp().data(ctx.dataKey, data).append(figure.root); }, refresh: function refresh(ctx, figure, data, proxy, settings) { figure.bubble.attr({ r: settings.size / 2 }); }, _getStyles: function _getStyles(styles, style) { var color = style.color || null, borderColor = style.borderColor || null, borderWidth = pick(style.borderWidth, null), opacity = pick(style.opacity, null); styles.bubble = [{ stroke: borderColor, "stroke-width": borderWidth, fill: color, opacity: opacity }, { stroke: style.hoveredBorderColor || borderColor, "stroke-width": pick(style.hoveredBorderWidth, borderWidth), fill: style.hoveredColor || style.color, opacity: pick(style.hoveredOpacity, opacity) }, { stroke: style.selectedBorderColor || borderColor, "stroke-width": pick(style.selectedBorderWidth, borderWidth), fill: style.selectedColor || style.color, opacity: pick(style.selectedOpacity, opacity) }]; }, _setState: function _setState(figure, styles, state) { applyElementState(figure, styles, state, "bubble"); }, arrange: function arrange(context, handles) { var values = [], i, ii = values.length = handles.length, settings = context.settings, dataField = settings.dataField, minSize = settings.minSize > 0 ? _Number(settings.minSize) : 0, maxSize = settings.maxSize > minSize ? _Number(settings.maxSize) : minSize, minValue, maxValue, deltaValue, deltaSize; if (settings.sizeGroups) { return; } for (i = 0; i < ii; ++i) { values[i] = _max(getDataValue(handles[i].proxy, dataField, "value") || 0, 0); } minValue = _min.apply(null, values); maxValue = _max.apply(null, values); deltaValue = maxValue - minValue || 1; deltaSize = maxSize - minSize; for (i = 0; i < ii; ++i) { handles[i]._settings.size = minSize + deltaSize * (values[i] - minValue) / deltaValue; } }, updateGrouping: function updateGrouping(context) { var dataField = context.settings.dataField; strategiesByType[TYPE_MARKER].updateGrouping(context); groupBySize(context, function (proxy) { return getDataValue(proxy, dataField, "value"); }); } }, pie: { _draw: function _draw(ctx, figure, data) { figure.pie = ctx.renderer.g().append(figure.root); figure.border = ctx.renderer.circle().sharp().data(ctx.dataKey, data).append(figure.root); }, refresh: function refresh(ctx, figure, data, proxy, settings) { var values = getDataValue(proxy, ctx.settings.dataField, "values") || [], i, ii = values.length || 0, colors = settings._colors, sum = 0, pie = figure.pie, renderer = ctx.renderer, dataKey = ctx.dataKey, r = (settings.size > 0 ? _Number(settings.size) : 0) / 2, start = 90, end = start; for (i = 0; i < ii; ++i) { sum += values[i] || 0; } for (i = 0; i < ii; ++i) { start = end; end += (values[i] || 0) / sum * 360; renderer.arc(0, 0, 0, r, start, end).attr({ "stroke-linejoin": "round", fill: colors[i] }).data(dataKey, data).append(pie); } figure.border.attr({ r: r }); }, _getStyles: function _getStyles(styles, style) { var opacity = pick(style.opacity, null), borderColor = style.borderColor || null, borderWidth = pick(style.borderWidth, null); styles.pie = [{ opacity: opacity }, { opacity: pick(style.hoveredOpacity, opacity) }, { opacity: pick(style.selectedOpacity, opacity) }]; styles.border = [{ stroke: borderColor, "stroke-width": borderWidth }, { stroke: style.hoveredBorderColor || borderColor, "stroke-width": pick(style.hoveredBorderWidth, borderWidth) }, { stroke: style.selectedBorderColor || borderColor, "stroke-width": pick(style.selectedBorderWidth, borderWidth) }]; }, _setState: function _setState(figure, styles, state) { applyElementState(figure, styles, state, "pie"); applyElementState(figure, styles, state, "border"); }, arrange: function arrange(context, handles) { var i, ii = handles.length, dataField = context.settings.dataField, values, count = 0, palette; for (i = 0; i < ii; ++i) { values = getDataValue(handles[i].proxy, dataField, "values"); if (values && values.length > count) { count = values.length; } } if (count > 0) { values = []; palette = context.params.themeManager.createPalette(context.settings.palette, { useHighlight: true, extensionMode: "alternate" }); for (i = 0; i < count; ++i) { values.push(palette.getNextColor()); } context.settings._colors = values; context.grouping.color = { callback: _noop, field: "", partition: [], values: [] }; context.params.dataExchanger.set(context.name, "color", { partition: [], values: values }); } } }, image: { _draw: function _draw(ctx, figure, data) { figure.image = ctx.renderer.image(null, null, null, null, null, "center").attr({ "pointer-events": "visible" }) // T567545 .data(ctx.dataKey, data).append(figure.root); }, refresh: function refresh(ctx, figure, data, proxy) { figure.image.attr({ href: getDataValue(proxy, ctx.settings.dataField, "url") }); }, _getStyles: function _getStyles(styles, style) { var size = style.size > 0 ? _Number(style.size) : 0, hoveredSize = size + (style.hoveredStep > 0 ? _Number(style.hoveredStep) : 0), selectedSize = size + (style.selectedStep > 0 ? _Number(style.selectedStep) : 0), opacity = pick(style.opacity, null); styles.image = [{ x: -size / 2, y: -size / 2, width: size, height: size, opacity: opacity }, { x: -hoveredSize / 2, y: -hoveredSize / 2, width: hoveredSize, height: hoveredSize, opacity: pick(style.hoveredOpacity, opacity) }, { x: -selectedSize / 2, y: -selectedSize / 2, width: selectedSize, height: selectedSize, opacity: pick(style.selectedOpacity, opacity) }]; }, _setState: function _setState(figure, styles, state) { applyElementState(figure, styles, state, "image"); } } }; function projectPoint(projection, coordinates) { return projection.project(coordinates); } function projectPointList(projection, coordinates) { var output = [], i, ii = output.length = coordinates.length; for (i = 0; i < ii; ++i) { output[i] = projection.project(coordinates[i]); } return output; } function projectLineString(projection, coordinates) { return [projectPointList(projection, coordinates)]; } function projectPolygon(projection, coordinates) { var output = [], i, ii = output.length = coordinates.length; for (i = 0; i < ii; ++i) { output[i] = projectPointList(projection, coordinates[i]); } return output; } function projectMultiPolygon(projection, coordinates) { var output = [], i, ii = output.length = coordinates.length; for (i = 0; i < ii; ++i) { output[i] = projectPolygon(projection, coordinates[i]); } return _concat.apply([], output); } function transformPoint(content, projection, coordinates) { var data = projection.transform(coordinates); content.root.attr({ translateX: data[0], translateY: data[1] }); } function transformList(projection, coordinates) { var output = [], i, ii = coordinates.length, item, k = 0; output.length = 2 * ii; for (i = 0; i < ii; ++i) { item = projection.transform(coordinates[i]); output[k++] = item[0]; output[k++] = item[1]; } return output; } function transformPointList(content, projection, coordinates) { var output = [], i, ii = output.length = coordinates.length; for (i = 0; i < ii; ++i) { output[i] = transformList(projection, coordinates[i]); } content.root.attr({ points: output }); } function transformAreaLabel(label, projection, coordinates) { var data = projection.transform(coordinates[0]); label.spaceSize = projection.getSquareSize(coordinates[1]); label.text.attr({ translateX: data[0], translateY: data[1] }); setAreaLabelVisibility(label); } function transformLineLabel(label, projection, coordinates) { var data = projection.transform(coordinates[0]); label.spaceSize = projection.getSquareSize(coordinates[1]); label.text.attr({ translateX: data[0], translateY: data[1] }); setLineLabelVisibility(label); } function getItemSettings(context, proxy, settings) { var result = combineSettings(context.settings, settings); proxy.text = proxy.text || settings.text; // DEPRECATED_15_2 applyGrouping(context.grouping, proxy, result); if (settings.color === undefined && settings.paletteIndex >= 0) { result.color = result._colors[settings.paletteIndex]; } return result; } function applyGrouping(grouping, proxy, settings) { _each(grouping, function (name, data) { var index = findGroupingIndex(data.callback(proxy, data.field), data.partition); if (index >= 0) { settings[name] = data.values[index]; } }); } function findGroupingIndex(value, partition) { var start = 0, end = partition.length - 1, index = -1, middle; if (partition[start] <= value && value <= partition[end]) { if (value === partition[end]) { index = end - 1; } else { while (end - start > 1) { middle = start + end >> 1; if (value < partition[middle]) { end = middle; } else { start = middle; } } index = start; } } return index; } function raiseChanged(context, handle, state, name) { context.params.eventTrigger(name, { target: handle.proxy, state: state }); } // This is required because `$.extend` cannot be used - because of the `options.data` which is commonly a very large array // TODO: Try to use our simple `extend` instead of `$.extend` function combineSettings(common, partial) { var obj = _extend({}, common, partial); obj.label = _extend({}, common.label, obj.label); obj.label.font = _extend({}, common.label.font, obj.label.font); return obj; } function processCommonSettings(type, options, themeManager) { var settings = combineSettings(themeManager.theme("layer:" + type) || { label: {} }, options), colors, i, palette; if (settings.paletteSize > 0) { palette = themeManager.createDiscretePalette(settings.palette, settings.paletteSize); for (i = 0, colors = []; i < settings.paletteSize; ++i) { colors.push(palette.getColor(i)); } settings._colors = colors; } return settings; } function valueCallback(proxy, dataField) { return proxy.attribute(dataField); } var performGrouping = function performGrouping(context, partition, settingField, dataField, valuesCallback) { var values; if (dataField && partition && partition.length > 1) { values = valuesCallback(partition.length - 1); context.grouping[settingField] = { callback: _isFunction(dataField) ? dataField : valueCallback, field: dataField, partition: partition, values: values }; context.params.dataExchanger.set(context.name, settingField, { partition: partition, values: values }); } }; function dropGrouping(context) { var name = context.name, dataExchanger = context.params.dataExchanger; _each(context.grouping, function (field) { dataExchanger.set(name, field, null); }); context.grouping = {}; } var groupByColor = function groupByColor(context) { performGrouping(context, context.settings.colorGroups, "color", context.settings.colorGroupingField, function (count) { var _palette = context.params.themeManager.createDiscretePalette(context.settings.palette, count), i, list = []; for (i = 0; i < count; ++i) { list.push(_palette.getColor(i)); } return list; }); }; var groupBySize = function groupBySize(context, valueCallback) { var settings = context.settings; performGrouping(context, settings.sizeGroups, "size", valueCallback || settings.sizeGroupingField, function (count) { var minSize = settings.minSize > 0 ? _Number(settings.minSize) : 0, maxSize = settings.maxSize >= minSize ? _Number(settings.maxSize) : 0, i = 0, sizes = []; if (count > 1) { for (i = 0; i < count; ++i) { sizes.push((minSize * (count - i - 1) + maxSize * i) / (count - 1)); } } else if (count === 1) { sizes.push((minSize + maxSize) / 2); } return sizes; }); }; function setFlag(flags, flag, state) { if (state) { flags |= flag; } else { flags &= ~flag; } return flags; } function hasFlag(flags, flag) { return !!(flags & flag); } function createLayerProxy(layer, name, index) { var proxy = { index: index, name: name, getElements: function getElements() { return layer.getProxies(); }, clearSelection: function clearSelection(_noEvent) { layer.clearSelection(_noEvent); return proxy; }, getDataSource: function getDataSource() { return layer.getDataSource(); } }; return proxy; } var MapLayer = function MapLayer(params, container, name, index) { var that = this; that._params = params; that._onProjection(); that.proxy = createLayerProxy(that, name, index); that._context = { name: name, layer: that.proxy, renderer: params.renderer, projection: params.projection, params: params, dataKey: params.dataKey, str: emptyStrategy, hover: false, selection: null, grouping: {}, // TODO: Link name should be built upon layer index rather than name root: params.renderer.g().attr({ "class": "dxm-layer" }).linkOn(container, name).linkAppend() }; that._container = container; that._options = {}; // Though the `_handles` field is set in the `_createHandles` it is required here because projection events are fired before data is set that._handles = []; // The `_data` field may be accessed in the `setOptions` when data is not set that._data = new EmptySource(); }; MapLayer.prototype = _extend({ constructor: MapLayer, _onProjection: function _onProjection() { var that = this; that._removeHandlers = that._params.projection.on({ "engine": function engine() { that._project(); }, "screen": function screen() { that._transform(); }, "center": function center() { that._transformCore(); }, "zoom": function zoom() { that._transform(); } }); }, _dataSourceLoadErrorHandler: function _dataSourceLoadErrorHandler() { this._dataSourceChangedHandler(); }, _dataSourceChangedHandler: function _dataSourceChangedHandler() { var that = this; that._data = unwrapFromDataSource(that._dataSource && that._dataSource.items()); that._update(true); }, _dataSourceOptions: function _dataSourceOptions() { return { paginate: false }; }, _getSpecificDataSourceOption: function _getSpecificDataSourceOption() { return this._specificDataSourceOption; }, _offProjection: function _offProjection() { this._removeHandlers(); this._removeHandlers = null; }, dispose: function dispose() { var that = this; that._disposeDataSource(); that._destroyHandles(); dropGrouping(that._context); that._context.root.linkRemove().linkOff(); that._context.labelRoot && that._context.labelRoot.linkRemove().linkOff(); that._context.str.reset(that._context); that._offProjection(); that._params = that._container = that._context = that.proxy = null; return that; }, ///#DEBUG TESTS_getContext: function TESTS_getContext() { return this._context; }, ///#ENDDEBUG setOptions: function setOptions(options) { var that = this, name; // DEPRECATED_15_2 options = that._options = options || {}; name = !("dataSource" in options) && "data" in options ? "data" : "dataSource"; // DEPRECATED_15_2 if (name in options && options[name] !== that._options_dataSource) { that._options_dataSource = options[name]; that._params.notifyDirty(); that._specificDataSourceOption = wrapToDataSource(options[name]); that._refreshDataSource(); } else if (that._data.count() > 0) { that._params.notifyDirty(); that._update(options.type !== undefined && options.type !== that._context.str.type || options.elementType !== undefined && options.elementType !== that._context.str.elementType); } that._transformCore(); }, _update: function _update(isContextChanged) { var that = this, context = that._context; if (isContextChanged) { context.str.reset(context); context.root.clear(); context.labelRoot && context.labelRoot.clear(); that._params.tracker.reset(); // T173037; TODO: There is no need to reset the entire tracker - only its memory about items that._destroyHandles(); context.str = selectStrategy(that._options, that._data); context.str.setup(context); that.proxy.type = context.str.type; that.proxy.elementType = context.str.elementType; } context.settings = processCommonSettings(context.str.fullType, that._options, that._params.themeManager); context.hasSeparateLabel = !!(context.settings.label.enabled && context.str.hasLabelsGroup); context.hover = !!_parseScalar(context.settings.hoverEnabled, true); // There is intentionally no attempt to preserve previous selection (or part of it) // Otherwise it would require some stack-like structure to keep selected items // Let's not complicate if (context.selection) { _each(context.selection.state, function (_, handle) { handle && handle.resetSelected(); }); } context.selection = getSelection(context.settings.selectionMode); if (context.hasSeparateLabel) { if (!context.labelRoot) { // TODO: Link name should be built upon layer index rather than name context.labelRoot = context.renderer.g().attr({ "class": "dxm-layer-labels" }).linkOn(that._container, { name: context.name + "-labels", after: context.name }).linkAppend(); that._transformCore(); } } else { if (context.labelRoot) { context.labelRoot.linkRemove().linkOff(); context.labelRoot = null; } } if (isContextChanged) { that._createHandles(); } dropGrouping(context); context.str.arrange(context, that._handles); context.str.updateGrouping(context); that._updateHandles(); that._params.notifyReady(); }, _destroyHandles: function _destroyHandles() { var handles = this._handles, i, ii = handles.length; for (i = 0; i < ii; ++i) { handles[i].dispose(); } if (this._context.selection) { this._context.selection.state = {}; } this._handles = []; }, _createHandles: function _createHandles() { var that = this, handles = that._handles = [], data = that._data, i, ii = handles.length = data.count(), context = that._context, geometry = data.geometry, attributes = data.attributes, handle, dataItem; for (i = 0; i < ii; ++i) { dataItem = data.item(i); handles[i] = new MapLayerElement(context, i, geometry(dataItem), attributes(dataItem)); } // Customization must be performed before anything else happens to element (that is the idea of customization) if (_isFunction(that._options.customize)) { (that._options._deprecated ? customizeHandles_deprecated : customizeHandles)(that.getProxies(), that._options.customize, that._params.widget); } // DEPRECATED_15_2 if (that._options._deprecated) { patchProxies(handles, context.name, data); } for (i = 0; i < ii; ++i) { handle = handles[i]; handle.project(); handle.draw(); handle.transform(); } if (context.selection) { _each(context.selection.state, function (_, handle) { handle && handle.restoreSelected(); }); } }, _updateHandles: function _updateHandles() { var handles = this._handles, i, ii = handles.length; for (i = 0; i < ii; ++i) { handles[i].refresh(); } if (this._context.settings.label.enabled) { for (i = 0; i < ii; ++i) { handles[i].measureLabel(); } for (i = 0; i < ii; ++i) { handles[i].adjustLabel(); } } }, _transformCore: function _transformCore() { var transform = this._params.projection.getTransform(); this._context.root.attr(transform); this._context.labelRoot && this._context.labelRoot.attr(transform); }, _project: function _project() { var handles = this._handles, i, ii = handles.length; for (i = 0; i < ii; ++i) { handles[i].project(); } }, _transform: function _transform() { var handles = this._handles, i, ii = handles.length; this._transformCore(); for (i = 0; i < ii; ++i) { handles[i].transform(); } }, getProxies: function getProxies() { var handles = this._handles, proxies = [], i, ii = proxies.length = handles.length; for (i = 0; i < ii; ++i) { proxies[i] = handles[i].proxy; } return proxies; }, getProxy: function getProxy(index) { return this._handles[index].proxy; }, raiseClick: function raiseClick(i, dxEvent) { this._params.eventTrigger("click", { target: this._handles[i].proxy, event: dxEvent }); }, hoverItem: function hoverItem(i, state) { this._handles[i].setHovered(state); }, selectItem: function selectItem(i, state, _noEvent) { this._handles[i].setSelected(state, _noEvent); }, clearSelection: function clearSelection() { var selection = this._context.selection; if (selection) { _each(selection.state, function (_, handle) { handle && handle.setSelected(false); }); selection.state = {}; } } }, DataHelperMixin); function createProxy(handle, coords, attrs) { var proxy = { coordinates: function coordinates() { return coords; }, attribute: function attribute(name, value) { if (arguments.length > 1) { attrs[name] = value; return proxy; } else { return arguments.length > 0 ? attrs[name] : attrs; } }, selected: function selected(state, _noEvent) { if (arguments.length > 0) { handle.setSelected(state, _noEvent); return proxy; } else { return handle.isSelected(); } }, applySettings: function applySettings(settings) { handle.update(settings); return proxy; } }; return proxy; } var MapLayerElement = function MapLayerElement(context, index, geometry, attributes) { var that = this, proxy = that.proxy = createProxy(that, geometry.coordinates, _extend({}, attributes)); that._ctx = context; that._index = index; that._fig = that._label = null; that._state = STATE_DEFAULT; that._coordinates = geometry.coordinates; that._settings = { label: {} }; proxy.index = index; proxy.layer = context.layer; // TODO: Replace "name" field with one referencing layer index and use layer index (instead of name) as layer id // as it is more suitable, simple and consistent that._data = { name: context.name, index: index }; }; MapLayerElement.prototype = { constructor: MapLayerElement, dispose: function dispose() { var that = this; that._ctx = that.proxy = that._settings = that._fig = that._label = that.data = null; return that; }, project: function project() { var context = this._ctx; this._projection = context.str.project(context.projection, this._coordinates); if (context.hasSeparateLabel && this._label) { this._projectLabel(); } }, _projectLabel: function _projectLabel() { this._labelProjection = this._ctx.str.projectLabel(this._projection); }, draw: function draw() { var that = this, context = this._ctx; context.str.draw(context, that._fig = {}, that._data); that._fig.root.append(context.root); }, transform: function transform() { var that = this, context = that._ctx; context.str.transform(that._fig, context.projection, that._projection); if (context.hasSeparateLabel && that._label) { that._transformLabel(); } }, _transformLabel: function _transformLabel() { this._ctx.str.transformLabel(this._label, this._ctx.projection, this._labelProjection); }, refresh: function refresh() { var that = this, strategy = that._ctx.str, settings = getItemSettings(that._ctx, that.proxy, that._settings); that._styles = strategy.getStyles(settings); strategy.refresh(that._ctx, that._fig, that._data, that.proxy, settings); that._refreshLabel(settings); that._setState(); }, _refreshLabel: function _refreshLabel(settings) { var that = this, context = that._ctx, labelSettings = settings.label, label = that._label; if (context.settings.label.enabled) { if (!label) { label = that._label = { root: context.labelRoot || that._fig.root, text: context.renderer.text().attr({ "class": "dxm-label" }), size: [0, 0] }; if (context.hasSeparateLabel) { that._projectLabel(); that._transformLabel(); } } label.value = _String(that.proxy.text || that.proxy.attribute(labelSettings.dataField) || ""); if (label.value) { // The data should be set when the element is created but it requires changes in the Renderer label.text.attr({ text: label.value, x: 0, y: 0 }).css(_patchFontOptions(labelSettings.font)).attr({ align: "center", stroke: labelSettings.stroke, "stroke-width": labelSettings["stroke-width"], "stroke-opacity": labelSettings["stroke-opacity"] }).data(context.dataKey, that._data).append(label.root); label.settings = settings; } } else { if (label) { label.text.remove(); that._label = null; } } }, measureLabel: function measureLabel() { var label = this._label, bBox; if (label.value) { bBox = label.text.getBBox(); label.size = [bBox.width, bBox.height, -bBox.y - bBox.height / 2]; } }, adjustLabel: function adjustLabel() { var label = this._label, offset; if (label.value) { offset = this._ctx.str.getLabelOffset(label, label.settings); label.settings = null; label.text.attr({ x: offset[0], y: offset[1] + label.size[2] }); } }, update: function update(settings) { var that = this; that._settings = combineSettings(that._settings, settings); // This check is required because the method can be called during the customization stage when DOM content neither is created nor should be changed if (that._fig) { that.refresh(); if (that._label && that._label.value) { that.measureLabel(); that.adjustLabel(); } } }, _setState: function _setState() { this._ctx.str.setState(this._fig, this._styles, STATE_TO_INDEX[this._state]); }, _setForeground: function _setForeground() { var root = this._fig.root; this._state ? root.toForeground() : root.toBackground(); }, setHovered: function setHovered(state) { var that = this, currentState = hasFlag(that._state, STATE_HOVERED), newState = !!state; if (that._ctx.hover && currentState !== newState) { that._state = setFlag(that._state, STATE_HOVERED, newState); that._setState(); that._setForeground(); raiseChanged(that._ctx, that, newState, "hoverChanged"); } return that; }, setSelected: function setSelected(state, _noEvent) { var that = this, currentState = hasFlag(that._state, STATE_SELECTED), newState = !!state, selection = that._ctx.selection, tmp; if (selection && currentState !== newState) { that._state = setFlag(that._state, STATE_SELECTED, newState); tmp = selection.state[selection.single]; selection.state[selection.single] = null; // This is to prevent stack overflow if (tmp) { tmp.setSelected(false); } selection.state[selection.single || that._index] = state ? that : null; // This check is required because the method can be called during the customization stage when DOM content neither is created nor should be changed if (that._fig) { that._setState(); that._setForeground(); if (!_noEvent) { raiseChanged(that._ctx, that, newState, "selectionChanged"); } } } }, isSelected: function isSelected() { return hasFlag(this._state, STATE_SELECTED); }, resetSelected: function resetSelected() { this._state = setFlag(this._state, STATE_SELECTED, false); }, restoreSelected: function restoreSelected() { this._fig.root.toForeground(); } }; // http://en.wikipedia.org/wiki/Centroid function calculatePolygonCentroid(coordinates) { var i, length = coordinates.length, v1, v2 = coordinates[length - 1], cross, cx = 0, cy = 0, area = 0, minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (i = 0; i < length; ++i) { v1 = v2; v2 = coordinates[i]; cross = v1[0] * v2[1] - v2[0] * v1[1]; area += cross; cx += (v1[0] + v2[0]) * cross; cy += (v1[1] + v2[1]) * cross; minX = _min(minX, v2[0]); maxX = _max(maxX, v2[0]); minY = _min(minY, v2[1]); maxY = _max(maxY, v2[1]); } // from centroid coords we need subtract the center of bbox coords to get a good geometrical center (T312029) return { area: _abs(area) / 2, center: [2 * cx / 3 / area - (minX + maxX) / 2, 2 * cy / 3 / area - (minY + maxY) / 2] }; } function calculateLineStringData(coordinates) { var i, ii = coordinates.length, v1, v2 = coordinates[0] || [], totalLength = 0, items = [0], min0 = v2[0], max0 = v2[0], min1 = v2[1], max1 = v2[1], t; for (i = 1; i < ii; ++i) { v1 = v2; v2 = coordinates[i]; totalLength += _sqrt((v1[0] - v2[0]) * (v1[0] - v2[0]) + (v1[1] - v2[1]) * (v1[1] - v2[1])); items[i] = totalLength; min0 = _min(min0, v2[0]); max0 = _max(max0, v2[0]); min1 = _min(min1, v2[1]); max1 = _max(max1, v2[1]); } i = findGroupingIndex(totalLength / 2, items); v1 = coordinates[i]; v2 = coordinates[i + 1]; t = (totalLength / 2 - items[i]) / (items[i + 1] - items[i]); return ii ? [[v1[0] * (1 - t) + v2[0] * t, v1[1] * (1 - t) + v2[1] * t], [max0 - min0, max1 - min1], totalLength] : []; } // TODO: Optimize! // There are redundant iterations in the following cycle - interior holes of a polygon should not be taken into account // So there is only centroid to be calculated for each "Polygon" function projectAreaLabel(coordinates) { var i, ii = coordinates.length, centroid, resultCentroid, maxArea = 0; for (i = 0; i < ii; ++i) { centroid = calculatePolygonCentroid(coordinates[i]); if (centroid.area > maxArea) { maxArea = centroid.area; resultCentroid = centroid; } } // TODO: Move "_sqrt" to the "calculatePolygonCentroid"