diff --git a/src/os/command/feature/abstractfeaturestylecmd.js b/src/os/command/feature/abstractfeaturestylecmd.js index 0d7842db9..7375733f1 100644 --- a/src/os/command/feature/abstractfeaturestylecmd.js +++ b/src/os/command/feature/abstractfeaturestylecmd.js @@ -1,5 +1,7 @@ goog.provide('os.command.AbstractFeatureStyle'); + goog.require('goog.events.Event'); +goog.require('os.action.EventType'); goog.require('os.command.AbstractStyle'); goog.require('os.command.ICommand'); goog.require('os.command.State'); @@ -67,7 +69,7 @@ os.command.AbstractFeatureStyle.prototype.applyValue = function(configs, value) * @inheritDoc */ os.command.AbstractFeatureStyle.prototype.finish = function(configs) { - os.dispatcher.dispatchEvent(new goog.events.Event(plugin.file.kml.KMLNodeLayerUICtrl.UIEventType.REFRESH)); + os.dispatcher.dispatchEvent(new goog.events.Event(os.action.EventType.REFRESH)); var feature = /** @type {ol.Feature} */ (this.getFeature()); var layer = os.MapContainer.getInstance().getLayer(this.layerId); goog.asserts.assert(layer, 'layer must be defined'); diff --git a/src/os/command/feature/featurecolorcmd.js b/src/os/command/feature/featurecolorcmd.js index 51c581b0f..402b9feb0 100644 --- a/src/os/command/feature/featurecolorcmd.js +++ b/src/os/command/feature/featurecolorcmd.js @@ -1,8 +1,10 @@ goog.provide('os.command.FeatureColor'); goog.require('os.command.AbstractFeatureStyle'); +goog.require('os.command.style'); goog.require('os.events.PropertyChangeEvent'); goog.require('os.metrics'); +goog.require('os.style'); @@ -13,20 +15,46 @@ goog.require('os.metrics'); * @param {string} layerId * @param {string} featureId * @param {Array|string} color - * @param {(Array|string)=} opt_oldColor + * @param {(Array|string|null)=} opt_oldColor + * @param {os.command.style.ColorChangeType=} opt_changeMode * @constructor */ -os.command.FeatureColor = function(layerId, featureId, color, opt_oldColor) { +os.command.FeatureColor = function(layerId, featureId, color, opt_oldColor, opt_changeMode) { + /** + * The color change mode. Determines how the config color is set. + * @type {os.command.style.ColorChangeType} + * @protected + */ + this.changeMode = opt_changeMode || os.command.style.ColorChangeType.COMBINED; + + // intentionally called after changeMode is set so getOldValue has the correct value os.command.FeatureColor.base(this, 'constructor', layerId, featureId, color, opt_oldColor); - this.title = 'Change Feature Color'; - this.metricKey = os.metrics.Layer.FEATURE_COLOR; + + switch (this.changeMode) { + case os.command.style.ColorChangeType.FILL: + this.title = 'Change Feature Fill Color'; + this.metricKey = os.metrics.Layer.FEATURE_FILL_COLOR; + this.defaultColor = os.style.DEFAULT_FILL_COLOR; + break; + case os.command.style.ColorChangeType.STROKE: + this.title = 'Change Feature Color'; + this.metricKey = os.metrics.Layer.FEATURE_COLOR; + this.defaultColor = os.style.DEFAULT_LAYER_COLOR; + break; + case os.command.style.ColorChangeType.COMBINED: + default: + this.title = 'Change Feature Color'; + this.metricKey = os.metrics.Layer.FEATURE_COLOR; + this.defaultColor = os.style.DEFAULT_LAYER_COLOR; + break; + } if (!color) { var feature = /** @type {ol.Feature} */ (this.getFeature()); var config = /** @type {Object|undefined} */ (feature.get(os.style.StyleType.FEATURE)); if (config) { - if (goog.isArray(config)) { + if (Array.isArray(config)) { config = config[0]; } var configColor = /** @type {Array|string|undefined} */ (os.style.getConfigColor(config)); @@ -42,31 +70,41 @@ os.command.FeatureColor = function(layerId, featureId, color, opt_oldColor) { goog.inherits(os.command.FeatureColor, os.command.AbstractFeatureStyle); -/** - * @type {string} - * @const - */ -os.command.FeatureColor.DEFAULT_COLOR = 'rgba(255,255,255,1)'; - - /** * @inheritDoc */ os.command.FeatureColor.prototype.getOldValue = function() { var feature = /** @type {ol.Feature} */ (this.getFeature()); var config = /** @type {Array|Object|undefined} */ (this.getFeatureConfigs(feature)); - if (goog.isArray(config)) { + if (Array.isArray(config)) { config = config[0]; } - return config ? os.style.getConfigColor(config) : os.command.FeatureColor.DEFAULT_COLOR; + var ret = this.defaultColor; + + if (config) { + switch (this.changeMode) { + case os.command.style.ColorChangeType.FILL: + ret = os.style.getConfigColor(config, false, os.style.StyleField.FILL); + break; + case os.command.style.ColorChangeType.STROKE: + ret = os.style.getConfigColor(config, false, os.style.StyleField.STROKE); + break; + case os.command.style.ColorChangeType.COMBINED: + default: + ret = os.style.getConfigColor(config); + break; + } + } + + return ret; }; /** * Gets the old label color * - * @return {Array|string|undefined} + * @return {Array|string} */ os.command.FeatureColor.prototype.getLabelValue = function() { var feature = /** @type {ol.Feature} */ (this.getFeature()); @@ -80,13 +118,48 @@ os.command.FeatureColor.prototype.getLabelValue = function() { */ os.command.FeatureColor.prototype.applyValue = function(configs, value) { var color = os.style.toRgbaString(/** @type {string} */ (value)); - for (var i = 0; i < configs.length; i++) { - os.style.setConfigColor(configs[i], color); - } - if (this.oldValue == this.getLabelValue()) { - this.applyLabelValue(configs, value); + // ignore opacity when comparing the label color to the current color + var labelColor = os.color.toHexString(this.getLabelValue()); + var currentColor = this.state === os.command.State.EXECUTING ? this.oldValue : this.value; + var currentHexColor = currentColor ? os.color.toHexString(currentColor) : null; + + switch (this.changeMode) { + case os.command.style.ColorChangeType.FILL: + for (var i = 0; i < configs.length; i++) { + os.style.setFillColor(configs[i], color); + } + break; + case os.command.style.ColorChangeType.STROKE: + for (var i = 0; i < configs.length; i++) { + var fillColor = os.style.getConfigColor(configs[i], false, os.style.StyleField.FILL); + + os.style.setConfigColor(configs[i], color); + + // preserve the original fill color when changing the stroke + if (fillColor) { + os.style.setFillColor(configs[i], os.style.toRgbaString(fillColor)); + } + } + + // if the label color matches the style color, change it as well + if (labelColor == currentHexColor) { + this.applyLabelValue(configs, color); + } + break; + case os.command.style.ColorChangeType.COMBINED: + default: + for (var i = 0; i < configs.length; i++) { + os.style.setConfigColor(configs[i], color); + } + + // if the label color matches the style color, change it as well + if (labelColor == currentHexColor) { + this.applyLabelValue(configs, color); + } + break; } + os.command.FeatureColor.base(this, 'applyValue', configs, value); }; @@ -112,14 +185,9 @@ os.command.FeatureColor.prototype.applyLabelValue = function(configs, value) { */ os.command.FeatureColor.prototype.finish = function(configs) { // dispatch the color change event on the source for the histogram - var feature = /** @type {ol.Feature} */ (this.getFeature()); - var source = /** @type {plugin.file.kml.KMLSource} */ (os.feature.getSource(feature)); - var rootNode = source.getRootNode(); - var children = rootNode.getChildren(); + var feature = this.getFeature(); - for (var i = 0; i < children.length; i++) { // update icon color - children[i].dispatchEvent(new os.events.PropertyChangeEvent('icons')); - } + feature.dispatchEvent(new os.events.PropertyChangeEvent('colors')); os.command.FeatureColor.base(this, 'finish', configs); }; diff --git a/src/os/command/feature/featureopacitycmd.js b/src/os/command/feature/featureopacitycmd.js index a8fce46c1..a2455cdfc 100644 --- a/src/os/command/feature/featureopacitycmd.js +++ b/src/os/command/feature/featureopacitycmd.js @@ -1,8 +1,11 @@ goog.provide('os.command.FeatureOpacity'); +goog.require('goog.asserts'); goog.require('os.command.AbstractFeatureStyle'); +goog.require('os.command.style'); +goog.require('os.events.PropertyChangeEvent'); goog.require('os.metrics'); -goog.require('os.ui'); +goog.require('os.style'); @@ -13,49 +16,77 @@ goog.require('os.ui'); * @param {string} layerId * @param {string} featureId * @param {number} opacity - * @param {number=} opt_oldOpacity + * @param {number|null=} opt_oldOpacity + * @param {os.command.style.ColorChangeType=} opt_changeMode * @constructor */ -os.command.FeatureOpacity = function(layerId, featureId, opacity, opt_oldOpacity) { - os.command.FeatureOpacity.base(this, 'constructor', layerId, featureId, opacity, opt_oldOpacity); - this.title = 'Change Feature Opacity'; - this.metricKey = os.metrics.Layer.FEATURE_OPACITY; +os.command.FeatureOpacity = function(layerId, featureId, opacity, opt_oldOpacity, opt_changeMode) { + goog.asserts.assert(opacity != null, 'opacity must be defined'); - if (!opacity) { - var feature = /** @type {ol.Feature} */ (this.getFeature()); - var config = /** @type {Object|undefined} */ (feature.get(os.style.StyleType.FEATURE)); + /** + * The opacity change mode. Determines how the config opacity is set. + * @type {os.command.style.ColorChangeType} + * @protected + */ + this.changeMode = opt_changeMode || os.command.style.ColorChangeType.COMBINED; - if (config) { - if (goog.isArray(config)) { - config = config[0]; - } - opacity = /** @type {number} */ (os.style.getConfigOpacityColor(config)); - } + // intentionally called after changeMode is set so getOldValue has the correct value + os.command.FeatureOpacity.base(this, 'constructor', layerId, featureId, opacity, opt_oldOpacity); + + switch (this.changeMode) { + case os.command.style.ColorChangeType.FILL: + this.title = 'Change Feature Fill Opacity'; + this.metricKey = os.metrics.Layer.FEATURE_FILL_OPACITY; + break; + case os.command.style.ColorChangeType.STROKE: + this.title = 'Change Feature Opacity'; + this.metricKey = os.metrics.Layer.FEATURE_OPACITY; + break; + case os.command.style.ColorChangeType.COMBINED: + default: + this.title = 'Change Feature Opacity'; + this.metricKey = os.metrics.Layer.FEATURE_OPACITY; + break; } - this.value = opacity; + if (this.value == null) { + this.value = os.style.DEFAULT_FILL_ALPHA; + } }; goog.inherits(os.command.FeatureOpacity, os.command.AbstractFeatureStyle); -/** - * @type {number} - * @const - */ -os.command.FeatureOpacity.DEFAULT_OPACITY = 1; - - /** * @inheritDoc */ os.command.FeatureOpacity.prototype.getOldValue = function() { + var ret; var feature = /** @type {ol.Feature} */ (this.getFeature()); var config = /** @type {Array|Object|undefined} */ (this.getFeatureConfigs(feature)); - if (goog.isArray(config)) { + if (Array.isArray(config)) { config = config[0]; } - return config ? os.style.getConfigOpacityColor(config) : os.command.FeatureOpacity.DEFAULT_OPACITY; + if (config) { + var color; + switch (this.changeMode) { + case os.command.style.ColorChangeType.FILL: + color = os.style.getConfigColor(config, true, os.style.StyleField.FILL); + ret = color && color.length === 4 ? color[3] : os.style.DEFAULT_FILL_ALPHA; + break; + case os.command.style.ColorChangeType.STROKE: + color = os.style.getConfigColor(config, true, os.style.StyleField.STROKE); + ret = color && color.length === 4 ? color[3] : os.style.DEFAULT_ALPHA; + break; + case os.command.style.ColorChangeType.COMBINED: + default: + color = os.style.getConfigColor(config, true); + ret = color && color.length === 4 ? color[3] : os.style.DEFAULT_ALPHA; + break; + } + } + + return ret; }; @@ -63,9 +94,94 @@ os.command.FeatureOpacity.prototype.getOldValue = function() { * @inheritDoc */ os.command.FeatureOpacity.prototype.applyValue = function(configs, value) { - for (var i = 0; i < configs.length; i++) { - os.style.setConfigOpacityColor(configs[i], value); + var color; + var i; + + switch (this.changeMode) { + case os.command.style.ColorChangeType.FILL: + for (i = 0; i < configs.length; i++) { + color = os.style.getConfigColor(configs[i], true, os.style.StyleField.FILL) || + os.style.getConfigColor(configs[i], true); + + if (color) { + color[3] = value; + os.style.setFillColor(configs[i], os.style.toRgbaString(color)); + } + } + break; + case os.command.style.ColorChangeType.STROKE: + for (i = 0; i < configs.length; i++) { + color = os.style.getConfigColor(configs[i], true, os.style.StyleField.STROKE) || + os.style.getConfigColor(configs[i], true); + + if (color) { + var fillColor = os.style.getConfigColor(configs[i], false, os.style.StyleField.FILL); + + color[3] = value; + + var colorStr = os.style.toRgbaString(color); + os.style.setConfigColor(configs[i], colorStr); + + // preserve the original fill color when changing the stroke + if (fillColor) { + os.style.setFillColor(configs[i], fillColor); + } + } + } + this.updateLabelOpacity(configs, value); + break; + case os.command.style.ColorChangeType.COMBINED: + default: + for (i = 0; i < configs.length; i++) { + color = os.style.getConfigColor(configs[i], true); + + if (color) { + color[3] = value; + + var colorStr = os.style.toRgbaString(color); + os.style.setConfigColor(configs[i], colorStr); + } + } + this.updateLabelOpacity(configs, value); + break; } os.command.FeatureOpacity.base(this, 'applyValue', configs, value); }; + + +/** + * Set the label color + * + * @param {Object} configs The style config + * @param {number} value The opacity value. + */ +os.command.FeatureOpacity.prototype.updateLabelOpacity = function(configs, value) { + var feature = /** @type {ol.Feature} */ (this.getFeature()); + if (feature) { + var labelColor = /** @type {Array|string|undefined} */ (feature.get(os.style.StyleField.LABEL_COLOR)) || + os.style.DEFAULT_LAYER_COLOR; + labelColor = os.color.toRgbArray(labelColor); + labelColor[3] = value; + + var labelColorStr = os.style.toRgbaString(labelColor); + feature.set(os.style.StyleField.LABEL_COLOR, labelColorStr); + + for (var i = 0; i < configs.length; i++) { + configs[i][os.style.StyleField.LABEL_COLOR] = labelColorStr; + } + } +}; + + +/** + * @inheritDoc + */ +os.command.FeatureOpacity.prototype.finish = function(configs) { + // dispatch the color change event on the source for the histogram + var feature = this.getFeature(); + + feature.dispatchEvent(new os.events.PropertyChangeEvent('colors')); + + os.command.FeatureOpacity.base(this, 'finish', configs); +}; diff --git a/src/os/command/stylecommand.js b/src/os/command/stylecommand.js new file mode 100644 index 000000000..9f824367f --- /dev/null +++ b/src/os/command/stylecommand.js @@ -0,0 +1,12 @@ +goog.provide('os.command.style'); + + +/** + * Change types for color commands. Determines how color/opacity is set in style configs. + * @enum {string} + */ +os.command.style.ColorChangeType = { + COMBINED: 'combined', + FILL: 'fill', + STROKE: 'stroke' +}; diff --git a/src/os/command/vectorlayercolorcmd.js b/src/os/command/vectorlayercolorcmd.js index 3661a95dd..14b3dca5d 100644 --- a/src/os/command/vectorlayercolorcmd.js +++ b/src/os/command/vectorlayercolorcmd.js @@ -1,11 +1,10 @@ goog.provide('os.command.VectorLayerColor'); goog.require('os.command.AbstractVectorStyle'); -goog.require('os.data.OSDataManager'); +goog.require('os.command.style'); goog.require('os.events.PropertyChangeEvent'); goog.require('os.metrics'); goog.require('os.source.PropertyChange'); -goog.require('os.ui'); @@ -16,18 +15,47 @@ goog.require('os.ui'); * @param {string} layerId * @param {Array|string} color * @param {(Array|string)=} opt_oldColor + * @param {os.command.style.ColorChangeType=} opt_changeMode * @constructor */ -os.command.VectorLayerColor = function(layerId, color, opt_oldColor) { +os.command.VectorLayerColor = function(layerId, color, opt_oldColor, opt_changeMode) { + /** + * The color change mode. Determines how the config color is set. + * @type {os.command.style.ColorChangeType|undefined} + * @protected + */ + this.changeMode = opt_changeMode; + + // intentionally called after changeMode is set so getOldValue has the correct value os.command.VectorLayerColor.base(this, 'constructor', layerId, color, opt_oldColor); - this.title = 'Change Color'; - this.metricKey = os.metrics.Layer.VECTOR_COLOR; + + if (this.changeMode === os.command.style.ColorChangeType.FILL) { + this.title = 'Change Layer Fill Color'; + this.metricKey = os.metrics.Layer.VECTOR_FILL_COLOR; + } else { + this.title = 'Change Layer Color'; + this.metricKey = os.metrics.Layer.VECTOR_COLOR; + } if (!color) { var layer = /** @type {os.layer.Vector} */ (os.MapContainer.getInstance().getLayer(this.layerId)); if (layer) { var options = layer.getLayerOptions(); - color = /** @type {string} */ (options && options['baseColor'] || os.command.VectorLayerColor.DEFAULT_COLOR); + color = /** @type {string} */ (options && options['baseColor'] || os.style.DEFAULT_LAYER_COLOR); + } else { + color = os.style.DEFAULT_LAYER_COLOR; + } + } + + // when changing the fill, preserve the old alpha value + if (this.changeMode === os.command.style.ColorChangeType.FILL) { + var config = os.style.StyleManager.getInstance().getLayerConfig(this.layerId); + if (config) { + var currentFill = os.style.getConfigColor(config, true, os.style.StyleField.FILL); + var currentFillAlpha = currentFill && currentFill.length === 4 ? currentFill[3] : os.style.DEFAULT_FILL_ALPHA; + + color = os.color.toRgbArray(color); + color[3] = currentFillAlpha; } } @@ -37,19 +65,20 @@ os.command.VectorLayerColor = function(layerId, color, opt_oldColor) { goog.inherits(os.command.VectorLayerColor, os.command.AbstractVectorStyle); -/** - * @type {string} - * @const - */ -os.command.VectorLayerColor.DEFAULT_COLOR = 'rgba(255,255,255,1)'; - - /** * @inheritDoc */ os.command.VectorLayerColor.prototype.getOldValue = function() { var config = os.style.StyleManager.getInstance().getLayerConfig(this.layerId); - return config ? os.style.getConfigColor(config) : os.command.VectorLayerColor.DEFAULT_COLOR; + + var ret; + if (this.changeMode === os.command.style.ColorChangeType.FILL) { + ret = config ? os.style.getConfigColor(config, false, os.style.StyleField.FILL) : os.style.DEFAULT_FILL_COLOR; + } else { + ret = config ? os.style.getConfigColor(config) : os.style.DEFAULT_LAYER_COLOR; + } + + return ret; }; @@ -57,9 +86,21 @@ os.command.VectorLayerColor.prototype.getOldValue = function() { * @inheritDoc */ os.command.VectorLayerColor.prototype.applyValue = function(config, value) { - var color = os.style.toRgbaString(/** @type {string} */ (value)); - os.style.setConfigColor(config, color); - os.ui.adjustIconSet(this.layerId, color); + if (this.changeMode === os.command.style.ColorChangeType.FILL) { + os.style.setFillColor(config, value); + } else { + // preserve the original fill color so the opacity isn't changed + var fillColor = os.style.getConfigColor(config, false, os.style.StyleField.FILL); + + // update the config color + os.style.setConfigColor(config, value); + + // restore the fill color + os.style.setFillColor(config, fillColor); + + // update the layer icons to reflect the color change + os.ui.adjustIconSet(this.layerId, value); + } os.command.VectorLayerColor.base(this, 'applyValue', config, value); }; diff --git a/src/os/command/vectorlayerfillopacitycmd.js b/src/os/command/vectorlayerfillopacitycmd.js new file mode 100644 index 000000000..d6e055b57 --- /dev/null +++ b/src/os/command/vectorlayerfillopacitycmd.js @@ -0,0 +1,53 @@ +goog.provide('os.command.VectorLayerFillOpacity'); + +goog.require('goog.asserts'); +goog.require('os.command.AbstractVectorStyle'); +goog.require('os.metrics'); + + +/** + * Changes the fill opacity of a vector layer. + * @param {string} layerId The layer id. + * @param {number} opacity The new fill opacity value. + * @param {number|null=} opt_oldOpacity The old fill opacity value. + * @extends {os.command.AbstractVectorStyle} + * @constructor + */ +os.command.VectorLayerFillOpacity = function(layerId, opacity, opt_oldOpacity) { + os.command.VectorLayerFillOpacity.base(this, 'constructor', layerId, opacity, opt_oldOpacity); + this.title = 'Change Layer Fill Opacity'; + this.metricKey = os.metrics.Layer.VECTOR_FILL_OPACITY; + + if (this.value == null) { + this.value = os.style.DEFAULT_FILL_ALPHA; + } +}; +goog.inherits(os.command.VectorLayerFillOpacity, os.command.AbstractVectorStyle); + + +/** + * @inheritDoc + */ +os.command.VectorLayerFillOpacity.prototype.getOldValue = function() { + var config = os.style.StyleManager.getInstance().getLayerConfig(this.layerId); + var color = os.style.getConfigColor(config, true, os.style.StyleField.FILL); + return color && color.length === 4 ? color[3] : os.style.DEFAULT_FILL_ALPHA; +}; + + +/** + * @inheritDoc + */ +os.command.VectorLayerFillOpacity.prototype.applyValue = function(config, value) { + var color = os.style.getConfigColor(config, true, os.style.StyleField.FILL) || + os.style.getConfigColor(config, true); + + if (color) { + color[3] = value; + + var colorString = os.style.toRgbaString(color); + os.style.setFillColor(config, colorString); + } + + os.command.VectorLayerFillOpacity.base(this, 'applyValue', config, value); +}; diff --git a/src/os/feature/feature.js b/src/os/feature/feature.js index 972e50bd2..50f2c0aef 100644 --- a/src/os/feature/feature.js +++ b/src/os/feature/feature.js @@ -741,43 +741,59 @@ os.feature.getSource = function(feature, opt_layer) { * * @param {ol.Feature} feature The feature. * @param {os.source.ISource=} opt_source The source containing the feature, or null to ignore source color. - * @param {*=} opt_default The default color. - * @return {*} The color. + * @param {(Array|string)=} opt_default The default color, `null` to indicate no color. + * @param {os.style.StyleField=} opt_colorField The style field to use in locating the color. + * @return {Array|string} The color. * * @suppress {accessControls} To allow direct access to feature metadata. */ -os.feature.getColor = function(feature, opt_source, opt_default) { +os.feature.getColor = function(feature, opt_source, opt_default, opt_colorField) { + var defaultColor = opt_default !== undefined ? opt_default : os.style.DEFAULT_LAYER_COLOR; + if (feature) { - var color = /** @type {string|undefined} */ (feature.values_[os.data.RecordField.COLOR]); + var color; + + if (!opt_colorField) { + // ignore the feature color override if a specific config color field was provided + color = /** @type {string|undefined} */ (feature.values_[os.data.RecordField.COLOR]); + } - if (!color) { + if (color !== defaultColor) { // check the layer config to see if it's replacing feature styles // the config here will not be modified, so get it directly from the manager for speed // (rather than getting a new one merged together for changing) var layerConfig = os.style.StyleManager.getInstance().getLayerConfig(os.feature.getLayerId(feature) || ''); if (layerConfig && layerConfig[os.style.StyleField.REPLACE_STYLE]) { - color = /** @type {string|undefined} */ (os.style.getConfigColor(layerConfig, false)); + color = os.style.getConfigColor(layerConfig, false, opt_colorField); } } - if (!color) { + if (color !== defaultColor) { var featureConfig = /** @type {Array|Object|undefined} */ (feature.values_[os.style.StyleType.FEATURE]); if (featureConfig) { if (Array.isArray(featureConfig)) { for (var i = 0; !color && i < featureConfig.length; i++) { - color = os.style.getConfigColor(featureConfig[i]) || undefined; + color = os.style.getConfigColor(featureConfig[i], false, opt_colorField); + + // stop on the first color found, allowing null if set as the default + if (color || (color === null && defaultColor === null)) { + break; + } } } else { - color = os.style.getConfigColor(featureConfig) || undefined; + color = os.style.getConfigColor(featureConfig, false, opt_colorField); } } } - if (color) { - return color; + if (color || (color === null && defaultColor === null)) { + // if the default color was provided as null, allow returning that as the color + return /** @type {Array|string} */ (color); } else if (opt_source) { + // fall back on the source color return opt_source.getColor(); } else if (opt_source !== null) { + // try to locate the source var source = os.feature.getSource(feature); if (source) { return source.getColor(); @@ -785,7 +801,33 @@ os.feature.getColor = function(feature, opt_source, opt_default) { } } - return opt_default !== undefined ? opt_default : os.style.DEFAULT_LAYER_COLOR; + return defaultColor; +}; + + +/** + * Get the fill color of a feature. + * @param {ol.Feature} feature The feature. + * @param {os.source.ISource=} opt_source The source containing the feature, or null to ignore source color. + * @param {(Array|string)=} opt_default The default color. + * @return {Array|string} The color. + */ +os.feature.getFillColor = function(feature, opt_source, opt_default) { + // default to null to indicate no fill + return os.feature.getColor(feature, opt_source, opt_default || null, os.style.StyleField.FILL); +}; + + +/** + * Get the stroke color of a feature. + * @param {ol.Feature} feature The feature. + * @param {os.source.ISource=} opt_source The source containing the feature, or null to ignore source color. + * @param {(Array|string)=} opt_default The default color. + * @return {Array|string} The color. + */ +os.feature.getStrokeColor = function(feature, opt_source, opt_default) { + // default to null to indicate no stroke + return os.feature.getColor(feature, opt_source, opt_default || null, os.style.StyleField.STROKE); }; diff --git a/src/os/layer/config/abstractlayerconfig.js b/src/os/layer/config/abstractlayerconfig.js index 6e9908df9..421ba6c3d 100644 --- a/src/os/layer/config/abstractlayerconfig.js +++ b/src/os/layer/config/abstractlayerconfig.js @@ -26,6 +26,12 @@ os.layer.config.AbstractLayerConfig = function() { */ this.color = os.style.DEFAULT_LAYER_COLOR; + /** + * @type {Array|string} + * @protected + */ + this.fillColor = 'rgba(255,255,255,0)'; + /** * @type {Array} * @protected diff --git a/src/os/layer/image.js b/src/os/layer/image.js index d5b004d70..fbae97402 100644 --- a/src/os/layer/image.js +++ b/src/os/layer/image.js @@ -411,7 +411,9 @@ os.layer.Image.prototype.getIcons = function() { if (config) { var color = os.style.getConfigColor(config, true); - return os.ui.createIconSet(this.getId(), this.getSVGSet(), this.getFASet(), color); + if (color) { + return os.ui.createIconSet(this.getId(), this.getSVGSet(), this.getFASet(), color); + } } return this.getIconSet().join(''); diff --git a/src/os/layer/vector.js b/src/os/layer/vector.js index db977dae2..356ba0b01 100644 --- a/src/os/layer/vector.js +++ b/src/os/layer/vector.js @@ -381,7 +381,9 @@ os.layer.Vector.prototype.getIcons = function() { if (config) { var color = os.style.getConfigColor(config, true); - return os.ui.createIconSet(this.getId(), this.getSVGSet(), this.getFASet(), color); + if (color) { + return os.ui.createIconSet(this.getId(), this.getSVGSet(), this.getFASet(), color); + } } return this.getIconSet().join(''); @@ -1129,6 +1131,7 @@ os.layer.Vector.prototype.persist = function(opt_to) { opt_to[os.style.StyleField.ARROW_SIZE] = config[os.style.StyleField.ARROW_SIZE]; opt_to[os.style.StyleField.ARROW_UNITS] = config[os.style.StyleField.ARROW_UNITS]; opt_to[os.style.StyleField.COLOR] = os.style.getConfigColor(config); + opt_to[os.style.StyleField.FILL_COLOR] = os.style.getConfigColor(config, false, os.style.StyleField.FILL); opt_to[os.style.StyleField.REPLACE_STYLE] = config[os.style.StyleField.REPLACE_STYLE]; opt_to[os.style.StyleField.SIZE] = os.style.getConfigSize(config); opt_to[os.style.StyleField.ICON] = os.style.getConfigIcon(config); @@ -1220,8 +1223,27 @@ os.layer.Vector.prototype.restore = function(config) { this.setMinResolution(config['minResolution'] || this.getMinResolution()); this.setMaxResolution(config['maxResolution'] || this.getMaxResolution()); - if (config[os.style.StyleField.COLOR] != null) { - os.style.setConfigColor(styleConf, os.style.toRgbaString(config[os.style.StyleField.COLOR])); + var color = config[os.style.StyleField.COLOR]; + if (color) { + os.style.setConfigColor(styleConf, os.style.toRgbaString(color)); + } + + var fillColor = config[os.style.StyleField.FILL_COLOR]; + + // if fill color is not defined, use the base color with default fill opacity + if (!fillColor && color) { + fillColor = os.color.toRgbArray(color); + fillColor[3] = os.style.DEFAULT_FILL_ALPHA; + } + + if (fillColor) { + // if a fill opacity is defined, override it in the color + if (config[os.style.StyleField.FILL_OPACITY] != null) { + fillColor = os.color.toRgbArray(fillColor); + fillColor[3] = config[os.style.StyleField.FILL_OPACITY]; + } + + os.style.setFillColor(styleConf, os.style.toRgbaString(fillColor)); } if (config[os.style.StyleField.REPLACE_STYLE] != null) { diff --git a/src/os/metrics/layersmetrics.js b/src/os/metrics/layersmetrics.js index a189250f2..b3e85defd 100644 --- a/src/os/metrics/layersmetrics.js +++ b/src/os/metrics/layersmetrics.js @@ -33,6 +33,11 @@ os.metrics.LayersMetrics = function() { description: 'The color used to render the data for this layer.', key: os.metrics.Layer.VECTOR_COLOR }); + this.addChild(styleLeaf, { + label: 'Change Feature Layer Fill Color', + description: 'The fill color used to render the data for this layer.', + key: os.metrics.Layer.VECTOR_FILL_COLOR + }); this.addChild(styleLeaf, { label: 'Change Feature Layer Shape', description: 'The icon shape used for data points on this layer.', @@ -53,6 +58,16 @@ os.metrics.LayersMetrics = function() { description: 'The icon size used for data points on this layer.', key: os.metrics.Layer.VECTOR_SIZE }); + this.addChild(styleLeaf, { + label: 'Change Feature Layer Opacity', + description: 'The opacity used to render the data for this layer.', + key: os.metrics.Layer.VECTOR_OPACITY + }); + this.addChild(styleLeaf, { + label: 'Change Feature Layer Fill Opacity', + description: 'The fill opacity used to render the data for this layer.', + key: os.metrics.Layer.VECTOR_FILL_OPACITY + }); this.addChild(styleLeaf, { label: 'Change Feature Layer Line Dash', description: 'The line dash for polygons and lines on this layer.', @@ -168,6 +183,11 @@ os.metrics.LayersMetrics = function() { description: 'The color used to render the data for this feature.', key: os.metrics.Layer.FEATURE_COLOR }); + this.addChild(styleLeaf, { + label: 'Change Feature Fill Color', + description: 'The fill color used to render the data for this feature.', + key: os.metrics.Layer.FEATURE_FILL_COLOR + }); this.addChild(styleLeaf, { label: 'Change Feature Icon', description: 'The icon used for data points on this feature.', @@ -178,6 +198,11 @@ os.metrics.LayersMetrics = function() { description: 'The opacity used to render the data for this feature.', key: os.metrics.Layer.FEATURE_OPACITY }); + this.addChild(styleLeaf, { + label: 'Change Feature Fill Opacity', + description: 'The fill opacity used to render the data for this feature.', + key: os.metrics.Layer.FEATURE_FILL_OPACITY + }); this.addChild(styleLeaf, { label: 'Change Feature Layer Size', description: 'The icon size used for data points on this feature.', diff --git a/src/os/metrics/metricskeys.js b/src/os/metrics/metricskeys.js index 084df8a1e..c414ac0af 100644 --- a/src/os/metrics/metricskeys.js +++ b/src/os/metrics/metricskeys.js @@ -237,10 +237,13 @@ os.metrics.keys.OS = { os.metrics.Layer = { FORCE_LAYER_COLOR: 'layers.features.forceLayerColor', VECTOR_COLOR: 'layers.features.changeVectorColor', + VECTOR_FILL_COLOR: 'layers.features.changeVectorFillColor', VECTOR_ICON: 'layers.features.changeVectorIcon', VECTOR_SHAPE: 'layers.features.changeVectorShape', VECTOR_CENTER_SHAPE: 'layers.features.changeVectorCenterShape', VECTOR_SIZE: 'layers.features.changeVectorSize', + VECTOR_OPACITY: 'layers.features.changeVectorOpacity', + VECTOR_FILL_OPACITY: 'layers.features.changeVectorFillOpacity', VECTOR_LINE_DASH: 'layers.features.changeVectorLineDash', VECTOR_AUTO_REFRESH: 'layers.features.changeVectorAutoRefresh', VECTOR_ELLIPSOID: 'layers.features.changeVectorEllipsoid', @@ -264,8 +267,10 @@ os.metrics.Layer = { VECTOR_LOB_BEARING_ERROR_COLUMN: 'layers.features.changeVectorLineOfBearingBearingErrorColumn', VECTOR_ROTATION_COLUMN: 'layers.features.changeVectorRotationColumn', FEATURE_COLOR: 'layers.features.changeFeatureColor', + FEATURE_FILL_COLOR: 'layers.features.changeFeatureFillColor', FEATURE_ICON: 'layers.features.changeFeatureIcon', FEATURE_OPACITY: 'layers.features.changeFeatureOpacity', + FEATURE_FILL_OPACITY: 'layers.features.changeFeatureFillOpacity', FEATURE_SIZE: 'layers.features.changeFeatureSize', FEATURE_LINE_DASH: 'layers.features.changeFeatureLineDash', FEATURE_LABEL_COLOR: 'layers.features.changeFeatureLabelColor', diff --git a/src/os/state/v4/baselayerstate.js b/src/os/state/v4/baselayerstate.js index 821c846d0..ae827356b 100644 --- a/src/os/state/v4/baselayerstate.js +++ b/src/os/state/v4/baselayerstate.js @@ -42,6 +42,8 @@ os.state.v4.LayerTag = { CSV_USE_HEADER: 'useHeader', DATA_PROVIDER: 'dataProvider', IS_BASE_LAYER: 'isBaseLayer', + FILL_COLOR: 'fillColor', + FILL_OPACITY: 'fillOpacity', LABEL_COLUMN: 'labelColumn', LABEL_COLUMNS: 'labelColumns', LABEL: 'label', @@ -470,6 +472,21 @@ os.state.v4.BaseLayerState.prototype.configKeyToXML = function(layerConfig, type this.defaultConfigToXML(key, value, layerEl); } break; + case os.style.StyleField.FILL_COLOR: + if (bfs) { + // hex string without the leading hash + var xmlColor = os.color.toServerString(/** @type {string} */ (value)); + os.xml.appendElement(os.state.v4.LayerTag.FILL_COLOR, bfs, xmlColor); + + // extract opacity from the color string + var colorArr = os.color.toRgbArray(/** @type {string} */ (value)); + var fillOpacity = colorArr.length == 4 ? colorArr[3] : os.style.DEFAULT_FILL_ALPHA; + os.xml.appendElement(os.state.v4.LayerTag.FILL_OPACITY, bfs, fillOpacity); + } else { + // tile layer + this.defaultConfigToXML(key, value, layerEl); + } + break; case 'contrast': if (typeof value === 'number' && !isNaN(value)) { // Cesium contrast: 0 is gray, 1 is normal, > 1 increases contrast. we allow from 0 to 2. @@ -933,6 +950,20 @@ os.state.v4.BaseLayerState.prototype.xmlToConfigKey = function(node, child, name options['size'] = goog.string.isNumeric(styleVal) ? Number(styleVal) / 2 : os.style.DEFAULT_FEATURE_SIZE; break; + case os.state.v4.LayerTag.FILL_COLOR: + try { + styleVal = os.color.padHexColor(styleVal, '#'); + options[os.style.StyleField.FILL_COLOR] = os.style.toRgbaString(styleVal); + } catch (e) { + } + break; + case os.state.v4.LayerTag.FILL_OPACITY: + var fillOpacity = Number(styleVal); + if (isNaN(fillOpacity)) { + fillOpacity = os.style.DEFAULT_FILL_ALPHA; + } + options[os.style.StyleField.FILL_OPACITY] = goog.math.clamp(fillOpacity, 0, 1); + break; case os.state.v4.LayerTag.LABEL_COLUMN: var column = typeof styleVal === 'string' ? goog.string.trim(styleVal) : ''; // Is this the default? diff --git a/src/os/style/style.js b/src/os/style/style.js index cad030ab0..f43e8f0bd 100644 --- a/src/os/style/style.js +++ b/src/os/style/style.js @@ -25,6 +25,14 @@ goog.require('os.style.StyleType'); os.style.DEFAULT_ALPHA = 1.0; +/** + * Default alpha for polygon fills. + * @type {number} + * @const + */ +os.style.DEFAULT_FILL_ALPHA = 0.0; + + /** * Default color for tile/feature layers. * @type {string} @@ -41,6 +49,14 @@ os.style.DEFAULT_LAYER_COLOR = 'rgba(255,255,255,1)'; os.style.DEFAULT_INVERSE_COLOR = 'rgba(255,0,0,1)'; +/** + * Default fill color for tile/feature layers. + * @type {string} + * @const + */ +os.style.DEFAULT_FILL_COLOR = 'rgba(255,255,255,0)'; + + /** * Default size for geometries. Radius for points. * @type {number} @@ -363,6 +379,9 @@ os.style.DEFAULT_VECTOR_CONFIG = { } }, // this will only be applied to line and polygon types + 'fill': { + 'color': os.style.DEFAULT_FILL_COLOR + }, 'stroke': { 'width': os.style.DEFAULT_STROKE_WIDTH, 'color': os.style.DEFAULT_LAYER_COLOR @@ -544,19 +563,33 @@ os.style.STYLE_COLOR_FIELDS_ = ['image', 'fill', 'stroke']; * * @param {Object} config The configuration to search for a color * @param {boolean=} opt_array If the color should be returned as an rgb array - * @param {(os.style.StyleField|string)=} opt_colorFieldHint A hint to where to find the color to use. - * @return {?(string|Array)} The color or null if none was found + * @param {os.style.StyleField=} opt_colorField The style field to use in locating the color. + * @return {Array|string|undefined} The color, or null if not found. Returns `undefined` if a style field was + * provided and the field was not present. */ -os.style.getConfigColor = function(config, opt_array, opt_colorFieldHint) { +os.style.getConfigColor = function(config, opt_array, opt_colorField) { if (config) { - if (opt_colorFieldHint && - config[opt_colorFieldHint] && - config[opt_colorFieldHint][os.style.StyleField.COLOR] != null) { - return opt_array ? os.color.toRgbArray(config[opt_colorFieldHint][os.style.StyleField.COLOR]) : - config[opt_colorFieldHint][os.style.StyleField.COLOR]; + // + // if a specific color field was provided, return: + // - null (no color) if the field is null + // - config..color if defined + // - undefined (no color found) + // + // otherwise: + // - return config.color if defined + // - search all color fields for the color + // + if (opt_colorField) { + if (config[opt_colorField] === null) { + return null; + } else if (config[opt_colorField] && config[opt_colorField][os.style.StyleField.COLOR] != null) { + return opt_array ? os.color.toRgbArray(config[opt_colorField][os.style.StyleField.COLOR]) : + config[opt_colorField][os.style.StyleField.COLOR]; + } + return undefined; } else if (config[os.style.StyleField.COLOR] != null) { return opt_array ? os.color.toRgbArray(config[os.style.StyleField.COLOR]) : - config[os.style.StyleField.COLOR]; + config[os.style.StyleField.COLOR]; } else { for (var i = 0; i < os.style.STYLE_COLOR_FIELDS_.length; i++) { var key = os.style.STYLE_COLOR_FIELDS_[i]; @@ -605,6 +638,31 @@ os.style.setConfigColor = function(config, color, opt_includeStyleFields) { }; +/** + * Sets the fill color, creating the fill object within the config if necessary. + * Colors are always set as an rgba string to minimize conversion both in opensphere style functions and OL3 rendering functions. + * + * @param {Object} config + * @param {Array|string|null|undefined} color + */ +os.style.setFillColor = function(config, color) { + if (config) { + if (!color) { + // no fill + config['fill'] = color; + } else if (!config['fill']) { + // adding fill + config['fill'] = { + 'color': color + }; + } else { + // changing fill color + config['fill']['color'] = color; + } + } +}; + + /** * Gets the icon used in a config. * @@ -1386,7 +1444,7 @@ os.style.createFeatureStyle = function(feature, baseConfig, opt_layerConfig) { // use label override color color = ol.color.asArray(opt_layerConfig[os.style.StyleField.LABEL_COLOR]); } else { - color = ol.color.asArray(os.style.getConfigColor(featureConfig)); + color = ol.color.asArray(os.style.getConfigColor(featureConfig) || os.style.DEFAULT_LAYER_COLOR); } color[3] *= opacity; labelStyle.text_.fill_.color_ = ol.color.toString(color); diff --git a/src/os/style/stylefield.js b/src/os/style/stylefield.js index 71294d164..af641dc31 100644 --- a/src/os/style/stylefield.js +++ b/src/os/style/stylefield.js @@ -11,6 +11,8 @@ os.style.StyleField = { CENTER_SHAPE: 'centerShape', COLOR: 'color', FILL: 'fill', + FILL_COLOR: 'fillColor', + FILL_OPACITY: 'fillOpacity', ICON: 'icon', IMAGE: 'image', LABELS: 'labels', diff --git a/src/os/ui/color/colorpicker.js b/src/os/ui/color/colorpicker.js index 7f6821073..e069a4e40 100644 --- a/src/os/ui/color/colorpicker.js +++ b/src/os/ui/color/colorpicker.js @@ -1,8 +1,8 @@ goog.provide('os.ui.color.ColorPickerCtrl'); goog.provide('os.ui.color.colorPickerDirective'); -goog.require('goog.color'); -goog.require('goog.color.alpha'); + goog.require('goog.events.EventType'); +goog.require('os.color'); goog.require('os.ui.Module'); goog.require('os.ui.color.ColorPaletteCtrl'); goog.require('os.ui.color.colorPaletteDirective'); @@ -100,10 +100,10 @@ os.ui.color.ColorPickerCtrl = function($scope, $element, $compile) { this['showPopup'] = false; // the control's color value should be undefined when the picker is set to the default color - var color = /** @type {string} */ (this.scope['color']) || undefined; - if (color && color.indexOf('#') !== 0) { - // if color was set to something other than a hex string (rgb() or rgba()), then fix it - color = (color.indexOf('a(') > -1 ? goog.color.alpha.parse(color) : goog.color.parse(color)).hex.substring(0, 7); + var color = /** @type {Array|string} */ (this.scope['color']) || undefined; + if (color) { + // ensure the color is a hex string + color = os.color.toHexString(color); } this.scope['color'] = color; diff --git a/src/os/ui/featureedit.js b/src/os/ui/featureedit.js index 385e7910a..ba6ad502b 100644 --- a/src/os/ui/featureedit.js +++ b/src/os/ui/featureedit.js @@ -15,6 +15,7 @@ goog.require('ol.geom.Point'); goog.require('os.MapContainer'); goog.require('os.action.EventType'); goog.require('os.data.ColumnDefinition'); +goog.require('os.geo'); goog.require('os.map'); goog.require('os.math.Units'); goog.require('os.ol.feature'); @@ -156,7 +157,7 @@ os.ui.FeatureEditCtrl = function($scope, $element, $timeout) { * The feature color. * @type {string} */ - this['color'] = os.style.DEFAULT_LAYER_COLOR; + this['color'] = os.color.toHexString(os.style.DEFAULT_LAYER_COLOR); /** * The feature opacity. @@ -170,6 +171,18 @@ os.ui.FeatureEditCtrl = function($scope, $element, $timeout) { */ this['size'] = os.style.DEFAULT_FEATURE_SIZE; + /** + * The feature fill color + * @type {string} + */ + this['fillColor'] = os.color.toHexString(os.style.DEFAULT_LAYER_COLOR); + + /** + * The feature fill opacity + * @type {string} + */ + this['fillOpacity'] = os.style.DEFAULT_FILL_ALPHA; + /** * The feature icon. * @type {!osx.icon.Icon} @@ -364,7 +377,7 @@ os.ui.FeatureEditCtrl = function($scope, $element, $timeout) { * Configured label color. * @type {string} */ - this['labelColor'] = os.style.DEFAULT_LAYER_COLOR; + this['labelColor'] = os.color.toHexString(os.style.DEFAULT_LAYER_COLOR); /** * Configured label size. @@ -466,6 +479,11 @@ os.ui.FeatureEditCtrl = function($scope, $element, $timeout) { this.previewFeature = feature; this.originalProperties_ = feature.getProperties(); + if (!this.isPolygon()) { + delete this['fillColor']; + delete this['fillOpacity']; + } + if (this.originalProperties_) { // we don't care about or want these sticking around, so remove them delete this.originalProperties_[os.style.StyleType.SELECT]; @@ -508,6 +526,10 @@ os.ui.FeatureEditCtrl = function($scope, $element, $timeout) { $scope.$watch('ctrl.labelColor', this.updatePreview.bind(this)); $scope.$watch('ctrl.labelSize', this.updatePreview.bind(this)); $scope.$watch('ctrl.showLabels', this.updatePreview.bind(this)); + $scope.$on('opacity.slide', this.onOpacityValueChange.bind(this)); + $scope.$on('fillColor.change', this.onFillColorChange.bind(this)); + $scope.$on('fillColor.reset', this.onFillColorReset.bind(this)); + $scope.$on('fillOpacity.slidestop', this.onFillOpacityChange.bind(this)); $scope.$on(os.ui.WindowEventType.CANCEL, this.onCancel.bind(this)); $scope.$on(os.ui.icon.IconPickerEventType.CHANGE, this.onIconChange.bind(this)); @@ -757,6 +779,23 @@ os.ui.FeatureEditCtrl.prototype.isEllipse = function() { }; +/** + * If a polygon is selected. + * + * @return {boolean} + * @export + */ +os.ui.FeatureEditCtrl.prototype.isPolygon = function() { + var geometry = this.previewFeature.getGeometry(); + + if (geometry) { + return os.geo.isGeometryPolygonal(geometry); + } else { + return false; + } +}; + + /** * If a line or polygon is selected. * @@ -933,9 +972,17 @@ os.ui.FeatureEditCtrl.prototype.createPreviewFeature = function() { 'alt': this['altitude'] }; } + + delete this['fillColor']; + delete this['fillOpacity']; } else { // not a point, so disable geometry edit this.originalGeometry = geometry; + + if (geometry instanceof ol.geom.LineString) { + delete this['fillColor']; + delete this['fillOpacity']; + } } // default feature to show the name field @@ -1014,6 +1061,18 @@ os.ui.FeatureEditCtrl.prototype.loadFromFeature = function(feature) { this['centerIcon'] = icon; } + // Make sure we have a fill color and opacity for polygons, and don't have them otherwise + if (config['fill'] && config['fill']['color']) { + if (this.isPolygon()) { + this['fillColor'] = os.color.toHexString(config['fill']['color']); + var opacity = os.color.toRgbArray(config['fill']['color']); + this['fillOpacity'] = opacity[3]; + } else { + delete this['fillColor']; + delete this['fillOpacity']; + } + } + var lineDash = os.style.getConfigLineDash(config); if (lineDash) { this['lineDash'] = lineDash; @@ -1254,6 +1313,15 @@ os.ui.FeatureEditCtrl.prototype.setFeatureConfig_ = function(config) { os.style.setConfigOpacityColor(config, 0); } + var fillColor = ol.color.asArray(this['fillColor']); + var fillOpacity = os.color.normalizeOpacity(this['fillOpacity']); + fillColor[3] = fillOpacity; + fillColor = os.style.toRgbaString(fillColor); + + if (color != fillColor) { + os.style.setFillColor(config, fillColor); + } + // set icon config if selected var useCenter = this.showCenterIcon(); if ((this['shape'] === os.style.ShapeType.ICON || useCenter) && config['image'] != null) { @@ -1395,8 +1463,14 @@ os.ui.FeatureEditCtrl.prototype.onColumnChange = function(event) { * @export */ os.ui.FeatureEditCtrl.prototype.onIconColorChange = function(opt_new, opt_old) { - if (opt_new != opt_old && this['labelColor'] == opt_old) { - this['labelColor'] = opt_new; + if (opt_new != opt_old) { + if (this['labelColor'] == opt_old) { + this['labelColor'] = opt_new; + } + + if (this['fillColor'] == opt_old) { + this['fillColor'] = opt_new; + } } this.updatePreview(); @@ -1449,6 +1523,60 @@ os.ui.FeatureEditCtrl.prototype.onLabelColorReset = function(event) { }; +/** + * Handles when the opacity slider has moved + * @param {angular.Scope.Event} event The Angular event. + * @param {number} value The new value. + * @export + */ +os.ui.FeatureEditCtrl.prototype.onOpacityValueChange = function(event, value) { + event.stopPropagation(); + + this['opacity'] = value; +}; + + +/** + * Handles when the fill opacity is changed + * @param {angular.Scope.Event} event The Angular event. + * @param {number} value The new value. + * @export + */ +os.ui.FeatureEditCtrl.prototype.onFillOpacityChange = function(event, value) { + event.stopPropagation(); + + this['fillOpacity'] = value; + this.updatePreview(); +}; + + +/** + * Handles when the fill color is changed + * @param {angular.Scope.Event} event The Angular event. + * @param {string} value + * @export + */ +os.ui.FeatureEditCtrl.prototype.onFillColorChange = function(event, value) { + event.stopPropagation(); + + this['fillColor'] = value; + this.updatePreview(); +}; + + +/** + * Handles fill color reset + * @param {angular.Scope.Event} event + * @export + */ +os.ui.FeatureEditCtrl.prototype.onFillColorReset = function(event) { + event.stopPropagation(); + + this['fillColor'] = this['color']; + this.updatePreview(); +}; + + /** * Get the minimum value for the semi-major ellipse axis by converting semi-minor to the semi-major units. * diff --git a/src/os/ui/file/kml/abstractkmlexporter.js b/src/os/ui/file/kml/abstractkmlexporter.js index 5dd8c3544..40e6458db 100644 --- a/src/os/ui/file/kml/abstractkmlexporter.js +++ b/src/os/ui/file/kml/abstractkmlexporter.js @@ -725,10 +725,13 @@ os.ui.file.kml.AbstractKMLExporter.prototype.addGeometryNode = function(item, no * @param {T} item The item * @param {string} styleId The style id * @param {string} color The item color + * @param {?string} fillColor The item fill color + * @param {?string} strokeColor The item fill color * @param {os.ui.file.kml.Icon=} opt_icon The item icon * @protected */ -os.ui.file.kml.AbstractKMLExporter.prototype.createStyle = function(item, styleId, color, opt_icon) { +os.ui.file.kml.AbstractKMLExporter.prototype.createStyle = function(item, styleId, color, fillColor, strokeColor, + opt_icon) { var styleEl = os.xml.createElementNS('Style', this.kmlNS, this.doc, undefined, { 'id': styleId }); @@ -763,14 +766,14 @@ os.ui.file.kml.AbstractKMLExporter.prototype.createStyle = function(item, styleI os.xml.appendElementNS('color', this.kmlNS, iconStyleEl, color); } - // all styles should define a line/poly style var lineStyleEl = os.xml.appendElementNS('LineStyle', this.kmlNS, styleEl); - os.xml.appendElementNS('color', this.kmlNS, lineStyleEl, color); + os.xml.appendElementNS('color', this.kmlNS, lineStyleEl, strokeColor || color); os.xml.appendElementNS('width', this.kmlNS, lineStyleEl, 2); var polyStyleEl = os.xml.appendElementNS('PolyStyle', this.kmlNS, styleEl); - os.xml.appendElementNS('color', this.kmlNS, polyStyleEl, color); - os.xml.appendElementNS('fill', this.kmlNS, polyStyleEl, 0); + os.xml.appendElementNS('color', this.kmlNS, polyStyleEl, fillColor || color); + os.xml.appendElementNS('fill', this.kmlNS, polyStyleEl, fillColor ? 1 : 0); + os.xml.appendElementNS('outline', this.kmlNS, polyStyleEl, strokeColor ? 1 : 0); var firstFolder = this.kmlDoc.querySelector('Folder'); if (firstFolder) { @@ -807,6 +810,26 @@ os.ui.file.kml.AbstractKMLExporter.prototype.getChildren = function(item) { os.ui.file.kml.AbstractKMLExporter.prototype.getColor = function(item) {}; +/** + * Get the fill color of an item. This should return an ABGR string that can be dropped directly into the KML. + * @abstract + * @param {T} item The item + * @return {?string} The item's fill color as an ABGR string + * @protected + */ +os.ui.file.kml.AbstractKMLExporter.prototype.getFillColor = function(item) {}; + + +/** + * Get the stroke color of an item. This should return an ABGR string that can be dropped directly into the KML. + * @abstract + * @param {T} item The item + * @return {?string} The item's stroke color as an ABGR string + * @protected + */ +os.ui.file.kml.AbstractKMLExporter.prototype.getStrokeColor = function(item) {}; + + /** * Get the type of KML element represented by the item. * @@ -996,6 +1019,16 @@ os.ui.file.kml.AbstractKMLExporter.prototype.getStyleId = function(item) { styleParts.push(color); } + var fillColor = this.getFillColor(item); + if (this.useItemColor || type == os.ui.file.kml.StyleType.DEFAULT) { + styleParts.push(fillColor); + } + + var strokeColor = this.getStrokeColor(item); + if (this.useItemColor || type == os.ui.file.kml.StyleType.DEFAULT) { + styleParts.push(strokeColor); + } + var icon = type == os.ui.file.kml.StyleType.DEFAULT ? undefined : this.getIcon(item); if (type == os.ui.file.kml.StyleType.ICON && this.useItemIcon) { // override the default icon with the item's icon @@ -1013,7 +1046,7 @@ os.ui.file.kml.AbstractKMLExporter.prototype.getStyleId = function(item) { styleId = styleParts.join('-'); if (!(styleId in this.styles_)) { - this.createStyle(item, styleId, color, icon); + this.createStyle(item, styleId, color, fillColor, strokeColor, icon); } return styleId; diff --git a/src/os/ui/layer/vectorlayerui.js b/src/os/ui/layer/vectorlayerui.js index 1ae97415c..cd0e1bc22 100644 --- a/src/os/ui/layer/vectorlayerui.js +++ b/src/os/ui/layer/vectorlayerui.js @@ -4,9 +4,12 @@ goog.provide('os.ui.layer.vectorLayerUIDirective'); goog.require('goog.color'); goog.require('os.MapChange'); goog.require('os.array'); +goog.require('os.command.LayerStyle'); +goog.require('os.command.SequenceCommand'); goog.require('os.command.VectorLayerAutoRefresh'); goog.require('os.command.VectorLayerCenterShape'); goog.require('os.command.VectorLayerColor'); +goog.require('os.command.VectorLayerFillOpacity'); goog.require('os.command.VectorLayerIcon'); goog.require('os.command.VectorLayerLabel'); goog.require('os.command.VectorLayerLabelColor'); @@ -19,6 +22,7 @@ goog.require('os.command.VectorLayerShowLabel'); goog.require('os.command.VectorLayerShowRotation'); goog.require('os.command.VectorLayerSize'); goog.require('os.command.VectorUniqueIdCmd'); +goog.require('os.command.style'); goog.require('os.data.OSDataManager'); goog.require('os.defines'); goog.require('os.style'); @@ -144,6 +148,12 @@ os.ui.layer.VectorLayerUICtrl = function($scope, $element, $timeout) { $scope.$on(os.ui.layer.VectorStyleControlsEventType.CENTER_SHAPE_CHANGE, this.onCenterShapeChange.bind(this)); $scope.$on(os.ui.layer.VectorStyleControlsEventType.LINE_DASH_CHANGE, this.onLineDashChange.bind(this)); + // New default added to the base constructor + this.defaults['fillOpacity'] = os.style.DEFAULT_FILL_ALPHA; + + $scope.$on('fillColor.change', this.onFillColorChange.bind(this)); + $scope.$on('fillColor.reset', this.onFillColorReset.bind(this)); + // label change handlers $scope.$on('labelColor.change', this.onLabelColorChange.bind(this)); $scope.$on('labelColor.reset', this.onLabelColorReset.bind(this)); @@ -165,6 +175,7 @@ os.ui.layer.VectorLayerUICtrl.prototype.initUI = function() { if (this.scope) { this.scope['color'] = this.getColor(); + this.scope['opacity'] = this.getOpacity(); this.scope['size'] = this.getSize(); this.scope['lineDash'] = this.getLineDash(); this.scope['icon'] = this.getIcon(); @@ -174,6 +185,8 @@ os.ui.layer.VectorLayerUICtrl.prototype.initUI = function() { this.scope['centerShape'] = this.getCenterShape(); this.scope['centerShapes'] = this.getCenterShapes(); this.scope['lockable'] = this.getLockable(); + this.scope['fillColor'] = this.getFillColor() || this.scope['color']; + this.scope['fillOpacity'] = this.getFillOpacity(); this['altitudeMode'] = this.getAltitudeMode(); this['columns'] = this.getColumns(); this['showRotation'] = this.getShowRotation(); @@ -239,6 +252,21 @@ os.ui.layer.VectorLayerUICtrl.prototype.getShapeUIInternal = function() { }; +/** + * @inheritDoc + */ +os.ui.layer.VectorLayerUICtrl.prototype.getProperties = function() { + return { + 'opacity': os.layer.setOpacity, + 'fillOpacity': goog.nullFunction, + 'brightness': os.layer.setBrightness, + 'contrast': os.layer.setContrast, + 'hue': os.layer.setHue, + 'saturation': os.layer.setSaturation + }; +}; + + /** * Decide when to show the rotation option * @@ -256,6 +284,19 @@ os.ui.layer.VectorLayerUICtrl.prototype.showRotationOption = function() { }; +/** + * Decide if we show the fill controls + * @return {boolean} + * @export + */ +os.ui.layer.VectorLayerUICtrl.prototype.showFillStyleControls = function() { + if (this.scope && this.scope['fillOpacity'] !== undefined) { + return true; + } + return false; +}; + + /** * Synchronizes the scope labels. * @@ -290,22 +331,51 @@ os.ui.layer.VectorLayerUICtrl.prototype.reconcileLabelsState_ = function() { os.ui.layer.VectorLayerUICtrl.prototype.onColorChange = function(event, value) { event.stopPropagation(); - var fn = - /** - * @param {os.layer.ILayer} layer - * @return {os.command.ICommand} - */ - function(layer) { - return new os.command.VectorLayerColor(layer.getId(), value); - }; + var color = this.getColor(); + var fillColor = this.getFillColor() || color; - this.createCommand(fn); + // if the color and fill color are the same, change both of them + if (color == fillColor) { + this.createCommand( + /** + * @param {os.layer.ILayer} layer + * @return {os.command.ICommand} + */ + function(layer) { + var cmds = []; + + // We run these sequentially so that they retain the different opacities + cmds.push(new os.command.VectorLayerColor( + layer.getId(), value, null, os.command.style.ColorChangeType.STROKE) + ); + cmds.push(new os.command.VectorLayerColor( + layer.getId(), value, null, os.command.style.ColorChangeType.FILL) + ); + + var sequence = new os.command.SequenceCommand(); + sequence.setCommands(cmds); + sequence.title = 'Change Color'; + + return sequence; + } + ); + } else { + this.createCommand( + /** + * @param {os.layer.ILayer} layer + * @return {os.command.ICommand} + */ + function(layer) { + return new os.command.VectorLayerColor( + layer.getId(), value, null, os.command.style.ColorChangeType.STROKE); + } + ); + } }; /** * Handles color reset - * * @param {angular.Scope.Event} event * @protected */ @@ -320,6 +390,89 @@ os.ui.layer.VectorLayerUICtrl.prototype.onColorReset = function(event) { }; +/** + * Handles changes to fill color + * @param {angular.Scope.Event} event + * @param {string} value + * @protected + */ +os.ui.layer.VectorLayerUICtrl.prototype.onFillColorChange = function(event, value) { + event.stopPropagation(); + + // If no value provided, set to the fill color + if (!value) { + value = this.getColor() || os.style.DEFAULT_FILL_COLOR; + } + + // Make sure the value includes the current opacity + var colorValue = os.color.toRgbArray(value); + colorValue[3] = this.scope['fillOpacity']; + + this.scope['fillColor'] = os.style.toRgbaString(colorValue); + + var fn = + /** + * @param {os.layer.ILayer} layer + * @return {os.command.ICommand} + */ + function(layer) { + return new os.command.VectorLayerColor( + layer.getId(), colorValue, null, os.command.style.ColorChangeType.FILL); + }; + + this.createCommand(fn); +}; + + +/** + * Handles fill color reset + * @param {angular.Scope.Event} event + * @protected + */ +os.ui.layer.VectorLayerUICtrl.prototype.onFillColorReset = function(event) { + event.stopPropagation(); + + // reset to match the base color + this.onFillColorChange(event, ''); +}; + + +/** + * @override + */ +os.ui.layer.VectorLayerUICtrl.prototype.onValueChange = function(callback, event, value) { + // If we are not dealing with fill opacity, let the parent handle this event + if (event.name == 'fillOpacity.slide') { + this.scope['fillOpacity'] = value; + } else { + os.ui.layer.VectorLayerUICtrl.base(this, 'onValueChange', callback, event, value); + } +}; + +/** + * @inheritDoc + */ +os.ui.layer.VectorLayerUICtrl.prototype.onSliderStop = function(callback, key, event, value) { + if (event && event.name == 'fillOpacity.slidestop') { + // Fill opacity must be changed at the style level + event.stopPropagation(); + + var fn = + /** + * @param {os.layer.ILayer} layer + * @return {os.command.ICommand} + */ + function(layer) { + return new os.command.VectorLayerFillOpacity(layer.getId(), value); + }; + + this.createCommand(fn); + } else { + os.ui.layer.VectorLayerUICtrl.base(this, 'onSliderStop', callback, key, event, value); + } +}; + + /** * Handles changes to size * @@ -579,6 +732,68 @@ os.ui.layer.VectorLayerUICtrl.prototype.getColor = function() { }; +/** + * Gets the fill color from the item(s) + * @return {?string} a hex color string + * @protected + */ +os.ui.layer.VectorLayerUICtrl.prototype.getFillColor = function() { + var items = /** @type {Array} */ (this.scope['items']); + + if (items) { + for (var i = 0, n = items.length; i < n; i++) { + var layer = items[i].getLayer(); + + if (layer) { + var config = os.style.StyleManager.getInstance().getLayerConfig(items[0].getId()); + + if (config) { + var color = os.style.getConfigColor(config, false, os.style.StyleField.FILL); + if (color) { + return os.color.toHexString(color); + } + } + } + } + } + + return null; +}; + + +/** + * Gets the fill opacity from the item(s) + * @return {?number} an opacity amount + * @protected + */ +os.ui.layer.VectorLayerUICtrl.prototype.getFillOpacity = function() { + var items = /** @type {Array} */ (this.scope['items']); + var opacity = os.style.DEFAULT_FILL_ALPHA; + + if (items) { + for (var i = 0, n = items.length; i < n; i++) { + var layer = items[i].getLayer(); + + if (layer) { + var config = os.style.StyleManager.getInstance().getLayerConfig(items[0].getId()); + + if (config) { + if (goog.isArray(config)) { + config = config[0]; + } + var color = os.style.getConfigColor(config, true, os.style.StyleField.FILL); + if (goog.isArray(color) && color.length >= 4) { + opacity = color[3]; + } + } + } + } + } + + return opacity; +}; + + /** * Gets the size from the item(s) * diff --git a/src/os/ui/layer/vectorstylecontrols.js b/src/os/ui/layer/vectorstylecontrols.js index 1df56f536..f09fa9ee4 100644 --- a/src/os/ui/layer/vectorstylecontrols.js +++ b/src/os/ui/layer/vectorstylecontrols.js @@ -30,7 +30,9 @@ os.ui.layer.vectorStyleControlsDirective = function() { replace: true, scope: { 'color': '=', + 'fillColor': '=?', 'opacity': '=', + 'fillOpacity': '=?', 'icon': '=?', 'centerIcon': '=?', 'iconSet': '=?', @@ -172,6 +174,19 @@ os.ui.layer.VectorStyleControlsCtrl.prototype.hasCenter = function() { }; +/** + * Decide if we show the fill controls + * @return {boolean} + * @export + */ +os.ui.layer.VectorStyleControlsCtrl.prototype.showFillStyleControls = function() { + if (this.scope && this.scope['fillOpacity'] !== undefined) { + return true; + } + return false; +}; + + /** * Fire a scope event when the ellipse center shape is changed by the user. * diff --git a/src/plugin/featureaction/featurestyleaction.js b/src/plugin/featureaction/featurestyleaction.js index a76cca4cb..e8a934150 100644 --- a/src/plugin/featureaction/featurestyleaction.js +++ b/src/plugin/featureaction/featurestyleaction.js @@ -21,6 +21,8 @@ plugin.im.action.feature.StyleActionTagName = { COLOR: 'color', ICON_SRC: 'iconSrc', ICON_OPTIONS: 'iconOptions', + FILL_COLOR: 'fillColor', + FILL_OPACITY: 'fillOpacity', LINE_DASH: 'lineDash', OPACITY: 'opacity', ROTATION_COLUMN: 'rotationColumn', @@ -197,6 +199,17 @@ plugin.im.action.feature.StyleAction.prototype.persist = function(opt_to) { plugin.im.action.feature.StyleAction.prototype.restore = function(config) { var styleConfig = /** @type {Object|undefined} */ (config['styleConfig']); if (styleConfig) { + // if the style config is lacking a fill, it's an old config prior to fill support. use the base color with the + // default fill opacity. + if (styleConfig['fill'] === undefined) { + var color = os.style.getConfigColor(styleConfig); + if (color) { + color = os.color.toRgbArray(color); + color[3] = os.style.DEFAULT_FILL_ALPHA; + os.style.setFillColor(styleConfig, os.style.toRgbaString(color)); + } + } + // create a new object in the same window context as this object this.styleConfig = {}; os.object.merge(styleConfig, this.styleConfig); @@ -214,7 +227,16 @@ plugin.im.action.feature.StyleAction.prototype.toXml = function() { if (color) { os.xml.appendElement(plugin.im.action.feature.StyleActionTagName.COLOR, element, os.color.toHexString(color)); os.xml.appendElement(plugin.im.action.feature.StyleActionTagName.OPACITY, element, - String(color.length > 3 ? color[3] : 1.0)); + String(color.length > 3 ? color[3] : os.style.DEFAULT_ALPHA)); + } + + var fillColor = /** @type {Array} */ + (os.style.getConfigColor(this.styleConfig, true, os.style.StyleField.FILL)); + if (fillColor) { + os.xml.appendElement(plugin.im.action.feature.StyleActionTagName.FILL_COLOR, element, + os.color.toHexString(fillColor)); + os.xml.appendElement(plugin.im.action.feature.StyleActionTagName.FILL_OPACITY, element, + String(fillColor.length > 3 ? fillColor[3] : os.style.DEFAULT_FILL_ALPHA)); } var size = os.style.getConfigSize(this.styleConfig); @@ -270,13 +292,14 @@ plugin.im.action.feature.StyleAction.prototype.fromXml = function(xml) { var styleConfig = /** @type {!Object} */ (os.object.unsafeClone(os.style.DEFAULT_VECTOR_CONFIG)); if (xml) { + var colorArr; var color = os.xml.getChildValue(xml, plugin.im.action.feature.StyleActionTagName.COLOR); if (os.color.isColorString(color)) { - var colorArr = os.color.toRgbArray(color); + colorArr = os.color.toRgbArray(color); if (colorArr) { var opacityVal = parseFloat( os.xml.getChildValue(xml, plugin.im.action.feature.StyleActionTagName.OPACITY)); - var opacity = !isNaN(opacityVal) ? goog.math.clamp(opacityVal, 0, 1) : 1.0; + var opacity = !isNaN(opacityVal) ? goog.math.clamp(opacityVal, 0, 1) : os.style.DEFAULT_ALPHA; colorArr[3] = opacity; color = os.style.toRgbaString(colorArr); } @@ -284,6 +307,28 @@ plugin.im.action.feature.StyleAction.prototype.fromXml = function(xml) { os.style.setConfigColor(styleConfig, color); } + var fillColor = os.xml.getChildValue(xml, plugin.im.action.feature.StyleActionTagName.FILL_COLOR); + if (os.color.isColorString(fillColor)) { + var fillColorArr = os.color.toRgbArray(fillColor); + if (fillColorArr) { + var fillOpacityVal = parseFloat( + os.xml.getChildValue(xml, plugin.im.action.feature.StyleActionTagName.FILL_OPACITY)); + var fillOpacity = !isNaN(fillOpacityVal) ? goog.math.clamp(fillOpacityVal, 0, 1) : os.style.DEFAULT_FILL_ALPHA; + fillColorArr[3] = fillOpacity; + fillColor = os.style.toRgbaString(fillColorArr); + } + } else if (colorArr) { + // No fill color in the XML, so use the base color with 0 opacity + var fillColorArr = colorArr.slice(); + fillColorArr[3] = 0; + fillColor = os.style.toRgbaString(fillColorArr); + } + + if (os.color.isColorString(fillColor)) { + // Only change the fill color without changing the image fill color too + os.style.setFillColor(styleConfig, fillColor); + } + var size = parseFloat(os.xml.getChildValue(xml, plugin.im.action.feature.StyleActionTagName.SIZE)); if (!isNaN(size)) { os.style.setConfigSize(styleConfig, size); diff --git a/src/plugin/featureaction/ui/featurestyleactionconfig.js b/src/plugin/featureaction/ui/featurestyleactionconfig.js index 9a9eefe9e..793cd8994 100644 --- a/src/plugin/featureaction/ui/featurestyleactionconfig.js +++ b/src/plugin/featureaction/ui/featurestyleactionconfig.js @@ -28,7 +28,7 @@ plugin.im.action.feature.ui.styleConfigDirective = function() { template: '
' + + 'show-color-reset="true" fill-color="fillColor" fill-opacity="fillOpacity">' + '
', controller: plugin.im.action.feature.ui.StyleConfigCtrl, @@ -89,7 +89,10 @@ plugin.im.action.feature.ui.StyleConfigCtrl = function($scope, $element) { $scope.$on('color.change', this.onColorChange.bind(this)); $scope.$on('color.reset', this.onColorReset.bind(this)); + $scope.$on('fillColor.change', this.onFillColorChange.bind(this)); + $scope.$on('fillColor.reset', this.onFillColorReset.bind(this)); $scope.$on('opacity.slidestop', this.onOpacityChange.bind(this)); + $scope.$on('fillOpacity.slidestop', this.onOpacityChange.bind(this)); $scope.$on('size.slidestop', this.onSizeChange.bind(this)); $scope.$on(os.ui.layer.VectorStyleControlsEventType.LINE_DASH_CHANGE, this.onLineDashChange.bind(this)); $scope.$on(os.ui.icon.IconPickerEventType.CHANGE, this.onIconChange.bind(this)); @@ -107,16 +110,52 @@ goog.inherits(plugin.im.action.feature.ui.StyleConfigCtrl, plugin.im.action.feat */ plugin.im.action.feature.ui.StyleConfigCtrl.prototype.initialize = function() { if (this.styleConfig) { - var color = /** @type {Array} */ (os.style.getConfigColor(this.styleConfig, true)); + var color = /** @type {Array} */ + (os.style.getConfigColor(this.styleConfig, true, os.style.StyleField.STROKE)); + + if (!color) { + color = os.style.getConfigColor(this.styleConfig, true); + } + if (color) { - this.scope['color'] = this.initialColor = goog.color.rgbArrayToHex(color); - this.scope['opacity'] = color.length > 3 ? color[3] : 1.0; + this.scope['color'] = this.initialColor = os.color.toHexString(color); + if (color.length < 4) { + color[3] = os.style.DEFAULT_ALPHA; + } + + this.scope['opacity'] = color[3]; } else { this.scope['color'] = os.color.toHexString(os.style.DEFAULT_LAYER_COLOR); - this.scope['opacity'] = 1.0; + this.scope['opacity'] = os.style.DEFAULT_ALPHA; + + color = os.color.toRgbArray(this.scope['color']); + color[3] = this.scope['opacity']; } - os.style.setConfigColor(this.styleConfig, os.style.getConfigColor(this.styleConfig)); + var fill = /** @type {Array} */ (os.style.getConfigColor(this.styleConfig, true, os.style.StyleField.FILL)); + + if (fill) { + this.scope['fillColor'] = os.color.toHexString(fill); + + if (fill.length < 4) { + fill[3] = os.style.DEFAULT_FILL_ALPHA; + } + + this.scope['fillOpacity'] = fill[3]; + } else { + this.scope['fillColor'] = os.color.toHexString(color || os.style.DEFAULT_FILL_COLOR); + this.scope['fillOpacity'] = os.style.DEFAULT_FILL_ALPHA; + + fill = os.color.toRgbArray(this.scope['fillColor']); + fill[3] = this.scope['fillOpacity']; + } + + // Standardize the stroke color + var strokeColor = os.style.toRgbaString(color); + os.style.setConfigColor(this.styleConfig, strokeColor); + + // If we have a fill color, set that to our style config + os.style.setFillColor(this.styleConfig, os.style.toRgbaString(fill)); this.scope['size'] = os.style.getConfigSize(this.styleConfig); this.scope['lineDash'] = os.style.getConfigLineDash(this.styleConfig); @@ -187,9 +226,71 @@ plugin.im.action.feature.ui.StyleConfigCtrl.prototype.onColorChange = function(e color = os.style.toRgbaString(colorArr); } - os.style.setConfigColor(this.styleConfig, color); + // update the fill color with the correct opacity + var fillColor = value ? os.style.toRgbaString(value) : os.style.DEFAULT_FILL_COLOR; + var fillOpacity = /** @type {number|undefined} */ (this.scope['fillOpacity']); + var fillColorArr = os.color.toRgbArray(fillColor); + if (fillColorArr && fillOpacity != null) { + fillColorArr[3] = fillOpacity; + fillColor = os.style.toRgbaString(fillColorArr); + } + + // Determine if we are changing both stroke and fill entirely, or keeping opacities separate, or only affecting stroke + var strokeColorHex = os.color.toHexString(this.scope['color']); + var fillColorHex = os.color.toHexString(this.scope['fillColor']); + var strokeColorOpacity = this.scope['opacity']; + var fillColorOpacity = this.scope['fillOpacity']; + + if (strokeColorHex == fillColorHex && strokeColorOpacity == fillColorOpacity) { + // change both to be the same + os.style.setConfigColor(this.styleConfig, color); + this.scope['color'] = os.color.toHexString(color); + this.scope['fillColor'] = os.color.toHexString(color); + } else if (strokeColorHex == fillColorHex) { + // change the color, but not the opacity, of each one separately + os.style.setConfigColor(this.styleConfig, color); + this.scope['color'] = os.color.toHexString(color); + + // Only change the fill color without changing the image fill color too + os.style.setFillColor(this.styleConfig, fillColor); + + this.scope['fillColor'] = os.color.toHexString(fillColor); + } else { + // change just the stroke color + os.style.setConfigColor(this.styleConfig, color); + this.scope['color'] = os.color.toHexString(color); + } + } +}; + + +/** + * Handle fill color change. + * + * @param {?angular.Scope.Event} event The Angular event. + * @param {string|undefined} value The new color value. + * @protected + */ +plugin.im.action.feature.ui.StyleConfigCtrl.prototype.onFillColorChange = function(event, value) { + if (event) { + event.stopPropagation(); + } + + if (this.styleConfig) { + var color = value ? os.style.toRgbaString(value) : os.style.DEFAULT_FILL_COLOR; + + // update the color with the correct opacity + var colorArr = os.color.toRgbArray(color); + var opacity = /** @type {number|undefined} */ (this.scope['fillOpacity']); + if (colorArr && opacity != null) { + colorArr[3] = opacity; + color = os.style.toRgbaString(colorArr); + } + + // Only change the fill color without changing the image fill color too + os.style.setFillColor(this.styleConfig, color); - this.scope['color'] = os.color.toHexString(color); + this.scope['fillColor'] = os.color.toHexString(color); } }; @@ -210,6 +311,22 @@ plugin.im.action.feature.ui.StyleConfigCtrl.prototype.onColorReset = function(ev }; +/** + * Handle fill color reset. + * + * @param {?angular.Scope.Event} event The Angular event. + * @protected + */ +plugin.im.action.feature.ui.StyleConfigCtrl.prototype.onFillColorReset = function(event) { + if (event) { + event.stopPropagation(); + } + + // reset the color to the initial value + this.onFillColorChange(event, os.style.DEFAULT_FILL_COLOR); +}; + + /** * Handle icon change. * @@ -240,8 +357,41 @@ plugin.im.action.feature.ui.StyleConfigCtrl.prototype.onOpacityChange = function event.stopPropagation(); } - if (this.styleConfig && value != null) { - os.style.setConfigOpacityColor(this.styleConfig, value); + var color; + + if (event.name == 'fillOpacity.slidestop') { + color = os.style.getConfigColor(this.styleConfig, true, os.style.StyleField.FILL); + color[3] = value; + + // Only change the fill color without changing the image fill color too + os.style.setFillColor(this.styleConfig, color); + + this.scope['fillOpacity'] = value; + } else { + var strokeColorHex = os.color.toHexString(this.scope['color']); + var fillColorHex = os.color.toHexString(this.scope['fillColor']); + var strokeColorOpacity = this.scope['opacity']; + var fillColorOpacity = this.scope['fillOpacity']; + + if (strokeColorHex == fillColorHex && strokeColorOpacity == fillColorOpacity) { + color = os.style.getConfigColor(this.styleConfig, true); + color[3] = value; + os.style.setConfigColor(this.styleConfig, color); + + this.scope['opacity'] = value; + this.scope['fillOpacity'] = value; + } else { + color = os.style.getConfigColor(this.styleConfig, true, os.style.StyleField.STROKE); + color[3] = value; + + os.style.setConfigColor(this.styleConfig, color); + + if (this.scope['fillColor']) { + os.style.setFillColor(this.styleConfig, this.styleConfig['fillColor']); + } + + this.scope['opacity'] = value; + } } }; diff --git a/src/plugin/file/kml/kmlexporter.js b/src/plugin/file/kml/kmlexporter.js index 2370485ec..8f98278f6 100644 --- a/src/plugin/file/kml/kmlexporter.js +++ b/src/plugin/file/kml/kmlexporter.js @@ -91,6 +91,24 @@ plugin.file.kml.KMLExporter.prototype.getColor = function(item) { }; +/** + * @inheritDoc + */ +plugin.file.kml.KMLExporter.prototype.getFillColor = function(item) { + var itemColor = os.feature.getFillColor(item, this.getSource_(item)); + return itemColor ? os.style.toAbgrString(itemColor) : null; +}; + + +/** + * @inheritDoc + */ +plugin.file.kml.KMLExporter.prototype.getStrokeColor = function(item) { + var itemColor = os.feature.getStrokeColor(item, this.getSource_(item)); + return itemColor ? os.style.toAbgrString(itemColor) : null; +}; + + /** * @inheritDoc */ diff --git a/src/plugin/file/kml/kmlfield.js b/src/plugin/file/kml/kmlfield.js index c4f824183..8825c4a36 100644 --- a/src/plugin/file/kml/kmlfield.js +++ b/src/plugin/file/kml/kmlfield.js @@ -58,6 +58,7 @@ plugin.file.kml.SOURCE_FIELDS = [ os.style.StyleField.LABELS, os.style.StyleField.LABEL_COLOR, os.style.StyleField.LABEL_SIZE, + os.style.StyleField.FILL_COLOR, os.style.StyleField.SHOW_LABELS ]; diff --git a/src/plugin/file/kml/kmlnodelayerui.js b/src/plugin/file/kml/kmlnodelayerui.js index c8a04202d..fa2e2b163 100644 --- a/src/plugin/file/kml/kmlnodelayerui.js +++ b/src/plugin/file/kml/kmlnodelayerui.js @@ -1,6 +1,7 @@ goog.provide('plugin.file.kml.KMLNodeLayerUICtrl'); -goog.provide('plugin.file.kml.KMLNodeLayerUICtrl.UIEventType'); goog.provide('plugin.file.kml.kmlNodeLayerUIDirective'); + +goog.require('os.action.EventType'); goog.require('os.command.FeatureCenterShape'); goog.require('os.command.FeatureColor'); goog.require('os.command.FeatureIcon'); @@ -13,7 +14,10 @@ goog.require('os.command.FeatureShape'); goog.require('os.command.FeatureShowLabel'); goog.require('os.command.FeatureSize'); goog.require('os.command.ParallelCommand'); +goog.require('os.command.SequenceCommand'); +goog.require('os.command.style'); goog.require('os.data.ColumnDefinition'); +goog.require('os.geo'); goog.require('os.ui.Module'); goog.require('os.ui.layer.VectorLayerUICtrl'); goog.require('os.ui.layer.iconStyleControlsDirective'); @@ -97,37 +101,42 @@ plugin.file.kml.KMLNodeLayerUICtrl = function($scope, $element, $timeout) { this['labelColumns'] = defaultColumns; $scope.$on('opacity.slide', this.onOpacityValueChange.bind(this)); - $scope.$on('opacity.slidestart', this.onSliderStart.bind(this)); $scope.$on('opacity.slidestop', this.onOpacityChange.bind(this)); + $scope.$on('fillOpacity.slide', this.onFillOpacityValueChange.bind(this)); + $scope.$on('fillOpacity.slidestop', this.onFillOpacityChange.bind(this)); - os.dispatcher.listen(plugin.file.kml.KMLNodeLayerUICtrl.UIEventType.REFRESH, this.initUI, false, this); + os.dispatcher.listen(os.action.EventType.REFRESH, this.initUI, false, this); }; goog.inherits(plugin.file.kml.KMLNodeLayerUICtrl, os.ui.layer.VectorLayerUICtrl); /** - * UI event types - * @enum {string} + * @inheritDoc */ -plugin.file.kml.KMLNodeLayerUICtrl.UIEventType = { - REFRESH: 'refresh' +plugin.file.kml.KMLNodeLayerUICtrl.prototype.disposeInternal = function() { + os.dispatcher.unlisten(os.action.EventType.REFRESH, this.initUI, false, this); + plugin.file.kml.KMLNodeLayerUICtrl.base(this, 'disposeInternal'); }; /** * @inheritDoc */ -plugin.file.kml.KMLNodeLayerUICtrl.prototype.disposeInternal = function() { - os.dispatcher.unlisten(os.ui.events.UIEventType.TOGGLE_UI, this.initUI, false, this); - plugin.file.kml.KMLNodeLayerUICtrl.base(this, 'disposeInternal'); +plugin.file.kml.KMLNodeLayerUICtrl.prototype.getProperties = function() { + return {}; // opacity set up in constructor }; /** * @inheritDoc */ -plugin.file.kml.KMLNodeLayerUICtrl.prototype.getProperties = function() { - return {}; // opacity set up in constructor +plugin.file.kml.KMLNodeLayerUICtrl.prototype.initUI = function() { + plugin.file.kml.KMLNodeLayerUICtrl.base(this, 'initUI'); + + if (this.scope && !this.isFeatureFillable()) { + delete this.scope['fillColor']; + delete this.scope['fillOpacity']; + } }; @@ -161,6 +170,66 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.getColor = function() { }; +/** + * @inheritDoc + */ +plugin.file.kml.KMLNodeLayerUICtrl.prototype.getFillColor = function() { + var items = /** @type {Array} */ (this.scope['items']); + + if (items) { + for (var i = 0, n = items.length; i < n; i++) { + var feature = items[i].getFeature(); + if (feature) { + var config = /** @type {Object|undefined} */ (feature.get(os.style.StyleType.FEATURE)); + + if (config) { + if (goog.isArray(config)) { + config = config[0]; + } + + var color = os.style.getConfigColor(config, false, os.style.StyleField.FILL); + if (color) { + return os.color.toHexString(color); + } + } + } + } + } + + return null; +}; + + +/** + * @inheritDoc + */ +plugin.file.kml.KMLNodeLayerUICtrl.prototype.getFillOpacity = function() { + var items = /** @type {Array} */ (this.scope['items']); + var opacity = os.style.DEFAULT_FILL_ALPHA; + + if (items) { + for (var i = 0, n = items.length; i < n; i++) { + var feature = items[i].getFeature(); + if (feature) { + var config = /** @type {Object|undefined} */ (feature.get(os.style.StyleType.FEATURE)); + + if (config) { + if (goog.isArray(config)) { + config = config[0]; + } + var color = os.style.getConfigColor(config, true, os.style.StyleField.FILL); + if (color) { + opacity = color[3]; + } + } + } + } + } + + return opacity; +}; + + /** * @inheritDoc */ @@ -339,7 +408,11 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.getOpacity = function() { if (goog.isArray(config)) { config = config[0]; } - opacity = os.style.getConfigOpacityColor(config) || os.style.DEFAULT_ALPHA; + + var color = os.style.getConfigColor(config, true, os.style.StyleField.STROKE); + if (color) { + opacity = color[3]; + } } } } @@ -454,7 +527,20 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.getLockable = function() { */ plugin.file.kml.KMLNodeLayerUICtrl.prototype.onOpacityValueChange = function(event, value) { event.stopPropagation(); - this.scope['opacity'] = value; // do not set this on the config - the command takes care of that + this.scope['opacity'] = value; +}; + + +/** + * Handle changes to fill opacity while it changes via slide controls + * + * @param {?angular.Scope.Event} event + * @param {?} value + * @protected + */ +plugin.file.kml.KMLNodeLayerUICtrl.prototype.onFillOpacityValueChange = function(event, value) { + event.stopPropagation(); + this.scope['fillOpacity'] = value; }; @@ -478,18 +564,124 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.onLockChange = function() { * @inheritDoc */ plugin.file.kml.KMLNodeLayerUICtrl.prototype.onColorChange = function(event, value) { + if (!os.color.isColorString(value) && !goog.isArray(value)) { + return; + } event.stopPropagation(); - var fn = + // Make sure the value includes the current opacity + var colorValue = os.color.toRgbArray(value); + colorValue[3] = this.scope['opacity']; + + // Do we have fill color/opacity to consider? + if (this.scope['fillColor'] !== undefined && this.scope['fillOpacity'] !== undefined) { + // Determine if we are changing both stroke and fill entirely, or keeping opacities separate, or only affecting stroke + if (this.scope['color'] == this.scope['fillColor'] && this.scope['opacity'] == this.scope['fillOpacity']) { + this.scope['color'] = os.color.toHexString(colorValue); + this.scope['fillColor'] = os.color.toHexString(colorValue); + + var fn = + /** + * @param {string} layerId + * @param {string} featureId + * @return {os.command.ICommand} + */ + function(layerId, featureId) { + return new os.command.FeatureColor(layerId, featureId, colorValue); + }; + + this.createFeatureCommand(fn); + } else if (this.scope['color'] == this.scope['fillColor']) { + this.scope['color'] = os.color.toHexString(colorValue); + this.scope['fillColor'] = os.color.toHexString(colorValue); + + // We create two commands so that they retain the different opacities + var strokeColor = os.color.toRgbArray(this.scope['color']); + strokeColor[3] = this.scope['opacity']; + var fillColor = os.color.toRgbArray(this.scope['fillColor']); + fillColor[3] = this.scope['fillOpacity']; + + var fn2 = + /** + * @param {string} layerId + * @param {string} featureId + * @return {os.command.ICommand} + */ + function(layerId, featureId) { + var cmds = []; + + cmds.push(new os.command.FeatureColor( + layerId, featureId, strokeColor, null, os.command.style.ColorChangeType.STROKE) + ); + cmds.push(new os.command.FeatureColor( + layerId, featureId, fillColor, null, os.command.style.ColorChangeType.FILL) + ); + + var sequence = new os.command.SequenceCommand(); + sequence.setCommands(cmds); + sequence.title = 'Change Color'; + + return sequence; + }; + + this.createFeatureCommand(fn2); + } else { + this.scope['color'] = os.color.toHexString(colorValue); + var fn3 = + /** + * @param {string} layerId + * @param {string} featureId + * @return {os.command.ICommand} + */ + function(layerId, featureId) { + return new os.command.FeatureColor(layerId, featureId, colorValue, null, + os.command.style.ColorChangeType.STROKE); + }; + + this.createFeatureCommand(fn3); + } + } else { + // We are not taking fill into consideration + var fn5 = /** * @param {string} layerId * @param {string} featureId * @return {os.command.ICommand} */ function(layerId, featureId) { - return new os.command.FeatureColor(layerId, featureId, value); + return new os.command.FeatureColor(layerId, featureId, colorValue); }; + this.createFeatureCommand(fn5); + } +}; + + +/** + * @inheritDoc + */ +plugin.file.kml.KMLNodeLayerUICtrl.prototype.onFillColorChange = function(event, value) { + if (!os.color.isColorString(value) && !goog.isArray(value)) { + return; + } + event.stopPropagation(); + + // Make sure the value includes the current opacity + var colorValue = os.color.toRgbArray(value); + colorValue[3] = this.scope['fillOpacity']; + + this.scope['fillColor'] = os.style.toRgbaString(colorValue); + + var fn = + /** + * @param {string} layerId + * @param {string} featureId + * @return {os.command.ICommand} + */ + function(layerId, featureId) { + return new os.command.FeatureColor(layerId, featureId, colorValue, null, os.command.style.ColorChangeType.FILL); + }; + this.createFeatureCommand(fn); }; @@ -599,16 +791,42 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.onCenterShapeChange = function(even /** - * Handles changes to color + * Handle changes to opacity while it changes via slide controls * - * @param {angular.Scope.Event} event + * @param {?angular.Scope.Event} event * @param {number} value * @protected */ plugin.file.kml.KMLNodeLayerUICtrl.prototype.onOpacityChange = function(event, value) { event.stopPropagation(); - if (value) { + if (value != null) { + var fn = + /** + * @param {string} layerId + * @param {string} featureId + * @return {os.command.ICommand} + */ + function(layerId, featureId) { + return new os.command.FeatureOpacity(layerId, featureId, value, null, os.command.style.ColorChangeType.STROKE); + }; + + this.createFeatureCommand(fn); + } +}; + + +/** + * Handle changes to fill opacity while it changes via slide controls + * + * @param {?angular.Scope.Event} event + * @param {number} value + * @protected + */ +plugin.file.kml.KMLNodeLayerUICtrl.prototype.onFillOpacityChange = function(event, value) { + event.stopPropagation(); + + if (value != null) { var fn = /** * @param {string} layerId @@ -616,7 +834,7 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.onOpacityChange = function(event, v * @return {os.command.ICommand} */ function(layerId, featureId) { - return new os.command.FeatureOpacity(layerId, featureId, value); + return new os.command.FeatureOpacity(layerId, featureId, value, null, os.command.style.ColorChangeType.FILL); }; this.createFeatureCommand(fn); @@ -827,10 +1045,9 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.createFeatureCommand = function(com if (feature && source) { var featureId = feature.getId(); var layerId = source.getId(); - if (featureId && layerId) { - if (typeof featureId == 'number') { - featureId = featureId.toString(); - } + if (featureId != null && layerId != null) { + featureId = String(featureId); + layerId = String(layerId); var cmd = commandFunction(layerId, featureId); if (cmd) { // if we have a feature and get a command, add it cmds.push(cmd); @@ -868,6 +1085,25 @@ plugin.file.kml.KMLNodeLayerUICtrl.prototype.isFeatureDynamic = function() { }; +/** + * If the feature is fillable, which means it should show fill controls + * + * @return {boolean} + * @export + */ +plugin.file.kml.KMLNodeLayerUICtrl.prototype.isFeatureFillable = function() { + var features = this.getFeatures(); + var feature = features.length > 0 ? features[0] : null; + if (feature) { + var geometry = feature.getGeometry(); + + return os.geo.isGeometryPolygonal(geometry); + } + + return false; +}; + + /** * Leave the rotation choices to the Place Add/Edit dialog since it is more involved * diff --git a/src/plugin/file/kml/kmltreeexporter.js b/src/plugin/file/kml/kmltreeexporter.js index f81e7c9df..1395a9f8a 100644 --- a/src/plugin/file/kml/kmltreeexporter.js +++ b/src/plugin/file/kml/kmltreeexporter.js @@ -94,6 +94,24 @@ plugin.file.kml.KMLTreeExporter.prototype.getColor = function(item) { }; +/** + * @inheritDoc + */ +plugin.file.kml.KMLTreeExporter.prototype.getFillColor = function(item) { + var featureColor = os.feature.getFillColor(item.getFeature()); + return featureColor ? os.style.toAbgrString(featureColor) : null; +}; + + +/** + * @inheritDoc + */ +plugin.file.kml.KMLTreeExporter.prototype.getStrokeColor = function(item) { + var featureColor = os.feature.getStrokeColor(item.getFeature()); + return featureColor ? os.style.toAbgrString(featureColor) : null; +}; + + /** * @inheritDoc */ diff --git a/src/plugin/file/kml/ui/kmlnode.js b/src/plugin/file/kml/ui/kmlnode.js index 5a875f169..bc0ea1dec 100644 --- a/src/plugin/file/kml/ui/kmlnode.js +++ b/src/plugin/file/kml/ui/kmlnode.js @@ -307,6 +307,10 @@ plugin.file.kml.ui.KMLNode.prototype.onFeatureChange = function(event) { this.dispatchEvent(new os.events.PropertyChangeEvent('icons')); this.dispatchEvent(new os.events.PropertyChangeEvent(os.annotation.EventType.CHANGE)); break; + case 'colors': + this.dispatchEvent(new os.events.PropertyChangeEvent('icons')); + this.dispatchEvent(new os.events.PropertyChangeEvent(os.annotation.EventType.CHANGE)); + break; default: break; } diff --git a/src/plugin/file/kml/ui/kmlui.js b/src/plugin/file/kml/ui/kmlui.js index 3e46383ef..61dae9ad2 100644 --- a/src/plugin/file/kml/ui/kmlui.js +++ b/src/plugin/file/kml/ui/kmlui.js @@ -2,11 +2,11 @@ goog.provide('plugin.file.kml.ui'); goog.require('goog.asserts'); goog.require('goog.events.Event'); +goog.require('os.action.EventType'); goog.require('os.command.SequenceCommand'); goog.require('os.object'); goog.require('os.style'); goog.require('plugin.file.kml.KMLField'); -goog.require('plugin.file.kml.KMLNodeLayerUICtrl.UIEventType'); goog.require('plugin.file.kml.cmd.KMLNodeAdd'); goog.require('plugin.file.kml.cmd.KMLNodeRemove'); goog.require('plugin.file.kml.kmlNodeLayerUIDirective'); @@ -169,7 +169,7 @@ plugin.file.kml.ui.updatePlacemark = function(options) { placemark.setLabel(feature.get(plugin.file.kml.KMLField.NAME) || 'Unnamed Place'); os.feature.update(feature); feature.changed(); - os.dispatcher.dispatchEvent(new goog.events.Event(plugin.file.kml.KMLNodeLayerUICtrl.UIEventType.REFRESH)); + os.dispatcher.dispatchEvent(new goog.events.Event(os.action.EventType.REFRESH)); } // add the placemark to a parent if provided diff --git a/src/plugin/places/places.js b/src/plugin/places/places.js index 3ec1769a1..7cbcaa033 100644 --- a/src/plugin/places/places.js +++ b/src/plugin/places/places.js @@ -67,6 +67,7 @@ plugin.places.ExportFields = [ os.style.StyleField.SHOW_LABEL_COLUMNS, os.style.StyleField.LABEL_COLOR, os.style.StyleField.LABEL_SIZE, + os.style.StyleField.FILL_COLOR, os.Fields.ALT, os.Fields.ALT_UNITS, os.data.RecordField.ALTITUDE_MODE, @@ -379,7 +380,14 @@ plugin.places.addPlace = function(options) { feature.set(os.style.StyleType.FEATURE, [styleConfig]); feature.set(os.style.StyleField.SHAPE, options.shape || os.style.ShapeType.POINT); - feature.set(os.style.StyleField.SHOW_LABELS, !!styleConfig && !!styleConfig['labels']); + + if (styleConfig && styleConfig[os.style.StyleField.LABELS]) { + os.feature.showLabel(feature); + os.ui.FeatureEditCtrl.persistFeatureLabels(feature); + } else { + os.feature.hideLabel(feature); + } + os.style.setFeatureStyle(feature); return parent ? plugin.file.kml.ui.updatePlacemark({ diff --git a/test/os/feature/feature.test.js b/test/os/feature/feature.test.js index 02f1ae6e0..84ffa8019 100644 --- a/test/os/feature/feature.test.js +++ b/test/os/feature/feature.test.js @@ -391,5 +391,189 @@ describe('os.feature', function() { feature.set('TITLE', 'test4'); expect(os.feature.getTitle(feature)).toBe('test4'); }); + + it('should get a color from a feature', function() { + var feature = new ol.Feature(); + var testColor = 'rgba(12,34,56,.1)'; + var sourceColor = 'rgba(98,76,54,.2)'; + var featureConfig1 = {}; + var featureConfig2 = { + color: testColor + }; + var source = { + getColor: function() { + return sourceColor; + } + }; + var getSourceSpy = spyOn(os.feature, 'getSource').andReturn(null); + + // should return the app default color when no feature provided + expect(os.feature.getColor(null)).toBe(os.style.DEFAULT_LAYER_COLOR); + // or when a feature provided + expect(os.feature.getColor(feature)).toBe(os.style.DEFAULT_LAYER_COLOR); + // unless a specific default color was specified + expect(os.feature.getColor(feature, undefined, testColor)).toBe(testColor); + // or a source was provided + expect(os.feature.getColor(feature, source)).toBe(sourceColor); + // or a source was available on the feature + getSourceSpy.andReturn(source); + expect(os.feature.getColor(feature)).toBe(sourceColor); + getSourceSpy.andReturn(null); + + // uses feature base color override + feature.set(os.data.RecordField.COLOR, testColor); + expect(os.feature.getColor(feature)).toBe(testColor); + feature.unset(os.data.RecordField.COLOR); + + // empty config returns default color + feature.set(os.style.StyleType.FEATURE, featureConfig1); + expect(os.feature.getColor(feature)).toBe(os.style.DEFAULT_LAYER_COLOR); + + // should return the base config color + featureConfig1.color = testColor; + expect(os.feature.getColor(feature)).toBe(testColor); + delete featureConfig1.color; + + // should return the fill color + featureConfig1.fill = { + color: testColor + }; + expect(os.feature.getColor(feature)).toBe(testColor); + delete featureConfig1.fill; + + // should return the image color + featureConfig1.image = { + color: testColor + }; + expect(os.feature.getColor(feature)).toBe(testColor); + delete featureConfig1.image; + + // should return the stroke color + featureConfig1.stroke = { + color: testColor, + width: 2 + }; + expect(os.feature.getColor(feature)).toBe(testColor); + delete featureConfig1.stroke; + + // should find a color in other configs + feature.set(os.style.StyleType.FEATURE, [featureConfig1, featureConfig2]); + expect(os.feature.getColor(feature)).toBe(testColor); + }); + + it('should get a fill color from a feature', function() { + var feature = new ol.Feature(); + var testColor = 'rgba(0,255,0,1)'; + var featureConfig1 = {}; + var featureConfig2 = { + fill: null + }; + + // defaults to null (no fill) when no feature provided + expect(os.feature.getFillColor(null)).toBeNull(); + // unless a default color was provided + expect(os.feature.getFillColor(null, undefined, os.style.DEFAULT_LAYER_COLOR)).toBe(os.style.DEFAULT_LAYER_COLOR); + + // defaults to null (no fill) + expect(os.feature.getFillColor(feature)).toBeNull(); + + // ignores feature base color override + feature.set(os.data.RecordField.COLOR, testColor); + expect(os.feature.getFillColor(feature)).toBeNull(); + + // empty config returns null + feature.set(os.style.StyleType.FEATURE, featureConfig1); + expect(os.feature.getFillColor(feature)).toBeNull(); + + // should not return the stroke/image color + featureConfig1.stroke = { + color: testColor, + width: 2 + }; + featureConfig1.image = { + color: testColor + }; + expect(os.feature.getFillColor(feature)).toBeNull(); + + // should not return the base config color + featureConfig1.color = testColor; + expect(os.feature.getFillColor(feature)).toBeNull(); + + // unless the fill is explicitly null + featureConfig1.fill = null; + expect(os.feature.getFillColor(feature)).toBeNull(); + + // gets the fill color + featureConfig1.fill = { + color: testColor + }; + expect(os.feature.getFillColor(feature)).toBe(testColor); + + // gets null from the first config + feature.set(os.style.StyleType.FEATURE, [featureConfig2, featureConfig1]); + expect(os.feature.getFillColor(feature)).toBeNull(); + + // gets the fill color from the second config + featureConfig2.fill = undefined; + expect(os.feature.getFillColor(feature)).toBe(testColor); + }); + + it('should get a stroke color from a feature', function() { + var feature = new ol.Feature(); + var testColor = 'rgba(0,255,0,1)'; + var featureConfig1 = {}; + var featureConfig2 = { + stroke: null + }; + + // defaults to null (no stroke) when no feature provided + expect(os.feature.getStrokeColor(null)).toBeNull(); + // unless a default color was provided + expect(os.feature.getStrokeColor(null, undefined, os.style.DEFAULT_LAYER_COLOR)) + .toBe(os.style.DEFAULT_LAYER_COLOR); + + // defaults to null (no stroke) + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // ignores feature base color override + feature.set(os.data.RecordField.COLOR, testColor); + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // empty config returns null + feature.set(os.style.StyleType.FEATURE, featureConfig1); + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // should not return the fill/image color + featureConfig1.fill = { + color: testColor + }; + featureConfig1.image = { + color: testColor + }; + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // should not return the base config color + featureConfig1.color = testColor; + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // unless the stroke is explicitly null + featureConfig1.stroke = null; + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // gets the stroke color + featureConfig1.stroke = { + color: testColor, + width: 2 + }; + expect(os.feature.getStrokeColor(feature)).toBe(testColor); + + // gets null from the first config + feature.set(os.style.StyleType.FEATURE, [featureConfig2, featureConfig1]); + expect(os.feature.getStrokeColor(feature)).toBeNull(); + + // gets the stroke color from the second config + featureConfig2.stroke = undefined; + expect(os.feature.getStrokeColor(feature)).toBe(testColor); + }); }); }); diff --git a/test/os/style/style.test.js b/test/os/style/style.test.js index f0d86f0bc..54dd73436 100644 --- a/test/os/style/style.test.js +++ b/test/os/style/style.test.js @@ -84,186 +84,277 @@ describe('os.style', function() { }); expect(os.style.getConfigIcon(config).path).toBe('newvalue'); }); -}); -describe('os.style.createFeatureStyle', function() { - var base; - var layer; - var opacity = 0.25; - - // base style opacity (0.99) should be multiplied by the feature opacity (0.25) - var featureRgba = 'rgba(0,255,100,0.2475)'; - var labelRgba = 'rgba(0,123,123,0.2475)'; - - beforeEach(function() { - base = { - 'image': { - 'type': 'circle', - 'radius': 2, - 'fill': { - 'color': 'rgba(0,255,100,0.99)' - } - }, - 'stroke': { - 'width': '3', - 'color': 'rgba(0,255,100,0.99)' - } + describe('os.style.getConfigColor', function() { + var testColor = 'rgba(12,34,56,.5)'; + var testColorArray = os.color.toRgbArray(testColor); + var colorConfig = { + color: testColor }; - - layer = { - 'image': { - 'type': 'circle', - 'radius': 3, - 'fill': { - 'color': 'rgba(0,255,100,0.99)' - } - }, - 'text': { - 'fill': { - 'color': 'rgba(0,255,0,0.99)' - } - }, - 'labelColor': 'rgba(0,123,123,0.99)', - 'labels': [{'column': 'some_field', 'showColumn': 'true'}] + var fillConfig = { + fill: colorConfig + }; + var imageConfig = { + image: colorConfig + }; + var strokeConfig = { + stroke: colorConfig + }; + var nullFillConfig = { + fill: null + }; + var nullImageConfig = { + image: null + }; + var nullStrokeConfig = { + stroke: null + }; + var undefinedFillConfig = { + fill: undefined + }; + var undefinedImageConfig = { + image: undefined + }; + var undefinedStrokeConfig = { + stroke: undefined }; - }); - it('should never ever modify the incoming config parameters', function() { - var baseExpected = JSON.stringify(base); - var layerExpected = JSON.stringify(layer); + it('should get the color from a config using default fields', function() { + expect(os.style.getConfigColor(null)).toBeNull(); + expect(os.style.getConfigColor({})).toBeNull(); - var feature = new ol.Feature(); - feature.set(os.style.StyleType.FEATURE, base); + expect(os.style.getConfigColor(colorConfig)).toBe(testColor); + expect(os.style.getConfigColor(fillConfig)).toBe(testColor); + expect(os.style.getConfigColor(imageConfig)).toBe(testColor); + expect(os.style.getConfigColor(strokeConfig)).toBe(testColor); + }); + + it('should get the color from a config using the array parameter', function() { + expect(os.style.getConfigColor(colorConfig, false)).toEqual(testColor); + expect(os.style.getConfigColor(fillConfig, false)).toEqual(testColor); + expect(os.style.getConfigColor(imageConfig, false)).toEqual(testColor); + expect(os.style.getConfigColor(strokeConfig, false)).toEqual(testColor); - expect(JSON.stringify(base)).toBe(baseExpected); - expect(JSON.stringify(layer)).toBe(layerExpected); + expect(os.style.getConfigColor(colorConfig, true)).toEqual(testColorArray); + expect(os.style.getConfigColor(fillConfig, true)).toEqual(testColorArray); + expect(os.style.getConfigColor(imageConfig, true)).toEqual(testColorArray); + expect(os.style.getConfigColor(strokeConfig, true)).toEqual(testColorArray); + }); + + it('should get the color from a config using a field hint parameter', function() { + // hint doesn't exist in config + expect(os.style.getConfigColor(colorConfig, undefined, os.style.StyleField.FILL)).toBeUndefined(); + expect(os.style.getConfigColor(imageConfig, undefined, os.style.StyleField.FILL)).toBeUndefined(); + expect(os.style.getConfigColor(strokeConfig, undefined, os.style.StyleField.FILL)).toBeUndefined(); + + expect(os.style.getConfigColor(colorConfig, undefined, os.style.StyleField.STROKE)).toBeUndefined(); + expect(os.style.getConfigColor(fillConfig, undefined, os.style.StyleField.STROKE)).toBeUndefined(); + expect(os.style.getConfigColor(imageConfig, undefined, os.style.StyleField.STROKE)).toBeUndefined(); + + expect(os.style.getConfigColor(colorConfig, undefined, os.style.StyleField.IMAGE)).toBeUndefined(); + expect(os.style.getConfigColor(fillConfig, undefined, os.style.StyleField.IMAGE)).toBeUndefined(); + expect(os.style.getConfigColor(strokeConfig, undefined, os.style.StyleField.IMAGE)).toBeUndefined(); + + // hint is defined in config + expect(os.style.getConfigColor(fillConfig, false, os.style.StyleField.FILL)).toBe(testColor); + expect(os.style.getConfigColor(imageConfig, false, os.style.StyleField.IMAGE)).toBe(testColor); + expect(os.style.getConfigColor(strokeConfig, false, os.style.StyleField.STROKE)).toBe(testColor); + + expect(os.style.getConfigColor(fillConfig, true, os.style.StyleField.FILL)).toEqual(testColorArray); + expect(os.style.getConfigColor(imageConfig, true, os.style.StyleField.IMAGE)).toEqual(testColorArray); + expect(os.style.getConfigColor(strokeConfig, true, os.style.StyleField.STROKE)).toEqual(testColorArray); + + // hint is undefined in config + expect(os.style.getConfigColor(undefinedFillConfig, undefined, os.style.StyleField.FILL)).toBeUndefined(); + expect(os.style.getConfigColor(undefinedImageConfig, undefined, os.style.StyleField.IMAGE)).toBeUndefined(); + expect(os.style.getConfigColor(undefinedStrokeConfig, undefined, os.style.StyleField.STROKE)).toBeUndefined(); + + // hint is null in config + expect(os.style.getConfigColor(nullFillConfig, undefined, os.style.StyleField.FILL)).toBeNull(); + expect(os.style.getConfigColor(nullImageConfig, undefined, os.style.StyleField.IMAGE)).toBeNull(); + expect(os.style.getConfigColor(nullStrokeConfig, undefined, os.style.StyleField.STROKE)).toBeNull(); + }); }); - it('should create styles with a new opacity value', function() { - var feature = new ol.Feature(); - feature.set(os.style.StyleType.FEATURE, base); - feature.set(os.style.StyleField.OPACITY, opacity); - feature.set('some_field', 'test'); + describe('os.style.createFeatureStyle', function() { + var base; + var layer; + var opacity = 0.25; + + // base style opacity (0.99) should be multiplied by the feature opacity (0.25) + var featureRgba = 'rgba(0,255,100,0.2475)'; + var labelRgba = 'rgba(0,123,123,0.2475)'; + + beforeEach(function() { + base = { + 'image': { + 'type': 'circle', + 'radius': 2, + 'fill': { + 'color': 'rgba(0,255,100,0.99)' + } + }, + 'stroke': { + 'width': '3', + 'color': 'rgba(0,255,100,0.99)' + } + }; + + layer = { + 'image': { + 'type': 'circle', + 'radius': 3, + 'fill': { + 'color': 'rgba(0,255,100,0.99)' + } + }, + 'text': { + 'fill': { + 'color': 'rgba(0,255,0,0.99)' + } + }, + 'labelColor': 'rgba(0,123,123,0.99)', + 'labels': [{'column': 'some_field', 'showColumn': 'true'}] + }; + }); + + it('should never ever modify the incoming config parameters', function() { + var baseExpected = JSON.stringify(base); + var layerExpected = JSON.stringify(layer); - var style = os.style.createFeatureStyle(feature, base, layer); + var feature = new ol.Feature(); + feature.set(os.style.StyleType.FEATURE, base); - // check all the colors and verify the rgba value - expect(style.length).toBe(2); - expect(style[0].image_.fill_.color_).toBe(featureRgba); - expect(style[0].stroke_.color_).toBe(featureRgba); - expect(style[1].text_.fill_.color_).toBe(labelRgba); + expect(JSON.stringify(base)).toBe(baseExpected); + expect(JSON.stringify(layer)).toBe(layerExpected); + }); + it('should create styles with a new opacity value', function() { + var feature = new ol.Feature(); + feature.set(os.style.StyleType.FEATURE, base); + feature.set(os.style.StyleField.OPACITY, opacity); + feature.set('some_field', 'test'); - // attempt the same again with an empty base config - // to make sure no exceptions are thrown - feature = new ol.Feature(); - feature.set(os.style.StyleType.FEATURE, {}); - feature.set(os.style.StyleField.OPACITY, opacity); - feature.set('some_field', 'test'); + var style = os.style.createFeatureStyle(feature, base, layer); - var style = os.style.createFeatureStyle(feature, {}, layer); + // check all the colors and verify the rgba value + expect(style.length).toBe(2); + expect(style[0].image_.fill_.color_).toBe(featureRgba); + expect(style[0].stroke_.color_).toBe(featureRgba); + expect(style[1].text_.fill_.color_).toBe(labelRgba); - // check all the colors and verify the rgba value - expect(style.length).toBe(2); - }); -}); -describe('os.style.mergeConfig', function() { - it('should merge basic style configs', function() { - var from = { - 'string': 'This is a test', - 'number': 1, - 'boolean': true - }; + // attempt the same again with an empty base config + // to make sure no exceptions are thrown + feature = new ol.Feature(); + feature.set(os.style.StyleType.FEATURE, {}); + feature.set(os.style.StyleField.OPACITY, opacity); + feature.set('some_field', 'test'); - var to = {}; + var style = os.style.createFeatureStyle(feature, {}, layer); - os.style.mergeConfig(from, to); - expect(to).toEqual(from); - expect(to).not.toBe(from); + // check all the colors and verify the rgba value + expect(style.length).toBe(2); + }); }); - it('should merge nested style configs', function() { - var from = { - 'nested': { - 'string': 'test', + describe('os.style.mergeConfig', function() { + it('should merge basic style configs', function() { + var from = { + 'string': 'This is a test', 'number': 1, 'boolean': true - }, - 'string': 'This is a test', - 'number': 2, - 'boolean': false - }; - - var to = {}; - os.style.mergeConfig(from, to); - expect(to).toEqual(from); - }); - - it('should overwrite when merging', function() { - var from = { - 'nested': { - 'egg': 2 - }, - 'string': 'test', - 'number': 1, - 'boolean': true - }; - - var toMergeAll = { - 'nested': { - 'egg': 1 - }, - 'string': 'mergeAll', - 'number': 0, - 'boolean': false, - 'other': 'no change' - }; + }; - os.style.mergeConfig(from, toMergeAll); - expect(toMergeAll).toEqual(ol.obj.assign({}, toMergeAll, from)); + var to = {}; - var toMergeSome = { - 'string': 'mergeSome', - 'number': -1 - }; - - os.style.mergeConfig(from, toMergeSome); - expect(toMergeSome).toEqual(from); + os.style.mergeConfig(from, to); + expect(to).toEqual(from); + expect(to).not.toBe(from); + }); - var toMergeSomeNested = { - 'nested': {}, - 'string': 'mergeSomeNested' - }; + it('should merge nested style configs', function() { + var from = { + 'nested': { + 'string': 'test', + 'number': 1, + 'boolean': true + }, + 'string': 'This is a test', + 'number': 2, + 'boolean': false + }; + + var to = {}; + os.style.mergeConfig(from, to); + expect(to).toEqual(from); + }); - os.style.mergeConfig(from, toMergeSomeNested); - expect(toMergeSomeNested).toEqual(from); - }); + it('should overwrite when merging', function() { + var from = { + 'nested': { + 'egg': 2 + }, + 'string': 'test', + 'number': 1, + 'boolean': true + }; + + var toMergeAll = { + 'nested': { + 'egg': 1 + }, + 'string': 'mergeAll', + 'number': 0, + 'boolean': false, + 'other': 'no change' + }; + + os.style.mergeConfig(from, toMergeAll); + expect(toMergeAll).toEqual(ol.obj.assign({}, toMergeAll, from)); + + var toMergeSome = { + 'string': 'mergeSome', + 'number': -1 + }; + + os.style.mergeConfig(from, toMergeSome); + expect(toMergeSome).toEqual(from); + + var toMergeSomeNested = { + 'nested': {}, + 'string': 'mergeSomeNested' + }; + + os.style.mergeConfig(from, toMergeSomeNested); + expect(toMergeSomeNested).toEqual(from); + }); - it('should use null for deletions', function() { - var from = {'value': null}; + it('should use null for deletions', function() { + var from = {'value': null}; - var to = {'value': {'color': 'red'}}; + var to = {'value': {'color': 'red'}}; - os.style.mergeConfig(from, to); - expect(to).toEqual(from); + os.style.mergeConfig(from, to); + expect(to).toEqual(from); - var newAddition = {'value': {'color': 'blue'}}; + var newAddition = {'value': {'color': 'blue'}}; - os.style.mergeConfig(newAddition, to); - expect(to).toEqual(newAddition); - }); + os.style.mergeConfig(newAddition, to); + expect(to).toEqual(newAddition); + }); - it('should use undefined for inheritence', function() { - // we've already tested implicit undefined above, so test explicit undefined - var to = {'value': undefined}; - var from = {'value': 1}; - os.style.mergeConfig(from, to); - expect(to).toEqual(from); - - var to = {'stroke': undefined}; - var from = {'stroke': {'color': 'red'}}; - os.style.mergeConfig(from, to); - expect(to).toEqual(from); + it('should use undefined for inheritence', function() { + // we've already tested implicit undefined above, so test explicit undefined + var to = {'value': undefined}; + var from = {'value': 1}; + os.style.mergeConfig(from, to); + expect(to).toEqual(from); + + var to = {'stroke': undefined}; + var from = {'stroke': {'color': 'red'}}; + os.style.mergeConfig(from, to); + expect(to).toEqual(from); + }); }); }); diff --git a/views/layer/vector.html b/views/layer/vector.html index 7989cbc3d..f141db940 100644 --- a/views/layer/vector.html +++ b/views/layer/vector.html @@ -8,7 +8,9 @@
- - Opacity - - - - Size - - - - Style - -
-
- -
-
- -
-
- - -
+
+
+ + +
+ +
+
+ +
+ + +
+
- - - - Border - - - - - - Center - - + +
+ +
+ + + +
+
+ +
+ +
+ +
+
+ +
+ + +
+ - - - +
+
+ +
+ + +
+ +
+ +
+ +
+
+
diff --git a/views/plugin/kml/kmlnodelayerui.html b/views/plugin/kml/kmlnodelayerui.html index 59aa3f236..5cf1ea307 100644 --- a/views/plugin/kml/kmlnodelayerui.html +++ b/views/plugin/kml/kmlnodelayerui.html @@ -9,6 +9,8 @@