/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.web.data.resource; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.ChoiceRenderer; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.FormComponentPanel; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.validation.IValidatable; import org.apache.wicket.validation.IValidator; import org.apache.wicket.validation.ValidationError; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.DimensionDefaultValueSetting; import org.geoserver.catalog.DimensionDefaultValueSetting.Strategy; import org.geoserver.catalog.DimensionInfo; import org.geoserver.catalog.DimensionPresentation; import org.geoserver.catalog.FeatureTypeInfo; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.impl.DimensionInfoImpl; import org.geoserver.ows.kvp.ElevationKvpParser; import org.geoserver.ows.kvp.TimeParser; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.web.wicket.ParamResourceModel; import org.geotools.coverage.grid.io.GridCoverage2DReader; import org.geotools.util.Range; import org.geotools.util.logging.Logging; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.feature.type.PropertyDescriptor; /** * Edits a {@link DimensionInfo} object for the specified resource * * @author Andrea Aime - GeoSolutions */ @SuppressWarnings("serial") public class DimensionEditor extends FormComponentPanel<DimensionInfo> { static final Logger LOGGER = Logging.getLogger(DimensionEditor.class); List<DimensionPresentation> presentationModes; List<DimensionDefaultValueSetting.Strategy> defaultValueStrategies; private CheckBox enabled; private DropDownChoice<String> attribute; private DropDownChoice<String> endAttribute; private DropDownChoice<DimensionPresentation> presentation; private DropDownChoice<DimensionDefaultValueSetting.Strategy> defaultValueStrategy; private TextField<String> referenceValue; private TextField<String> units; private TextField<String> unitSymbol; private PeriodEditor resTime; private TextField<BigDecimal> resElevation; boolean time; public DimensionEditor(String id, IModel<DimensionInfo> model, ResourceInfo resource, Class<?> type) { super(id, model); // double container dance to get stuff to show up and hide on demand (grrr) final WebMarkupContainer configsContainer = new WebMarkupContainer("configContainer"); configsContainer.setOutputMarkupId(true); add(configsContainer); final WebMarkupContainer configs = new WebMarkupContainer("configs"); configs.setOutputMarkupId(true); configs.setVisible(getModelObject().isEnabled()); configsContainer.add(configs); // enabled flag, and show the rest only if enabled is true final PropertyModel<Boolean> enabledModel = new PropertyModel<Boolean>(model, "enabled"); enabled = new CheckBox("enabled", enabledModel); add(enabled); enabled.add(new AjaxFormComponentUpdatingBehavior("click") { @Override protected void onUpdate(AjaxRequestTarget target) { Boolean visile = enabled.getModelObject(); configs.setVisible(visile); target.add(configsContainer); } }); // error message label Label noAttributeMessage = new Label("noAttributeMsg", ""); add(noAttributeMessage); // the attribute label and dropdown container WebMarkupContainer attContainer = new WebMarkupContainer("attributeContainer"); configs.add(attContainer); // check the attributes and show a dropdown List<String> attributes = getAttributesOfType(resource, type); attribute = new DropDownChoice<String>("attribute", new PropertyModel<String>(model, "attribute"), attributes); attribute.setOutputMarkupId(true); attribute.setRequired(true); attContainer.add(attribute); List<String> endAttributes = new ArrayList<String>(attributes); endAttributes.add(0, "-"); endAttribute = new DropDownChoice<String>("endAttribute", new PropertyModel<String>(model, "endAttribute"), endAttributes); endAttribute.setOutputMarkupId(true); endAttribute.setRequired(false); attContainer.add(endAttribute); // do we show it? if(resource instanceof FeatureTypeInfo) { if (attributes.isEmpty()) { disableDimension(type, configs, noAttributeMessage); } else { noAttributeMessage.setVisible(false); } } else if(resource instanceof CoverageInfo) { attContainer.setVisible(false); attribute.setRequired(false); try { GridCoverageReader reader = ((CoverageInfo) resource).getGridCoverageReader(null, null); if(Number.class.isAssignableFrom(type)) { String elev = reader.getMetadataValue(GridCoverage2DReader.HAS_ELEVATION_DOMAIN); if(!Boolean.parseBoolean(elev)) { disableDimension(type, configs, noAttributeMessage); } } else if(Date.class.isAssignableFrom(type)) { String time = reader.getMetadataValue(GridCoverage2DReader.HAS_TIME_DOMAIN); if(!Boolean.parseBoolean(time)) { disableDimension(type, configs, noAttributeMessage); } } } catch(IOException e) { throw new WicketRuntimeException(e); } } // units block final WebMarkupContainer unitsContainer = new WebMarkupContainer("unitsContainer"); configs.add(unitsContainer); IModel<String> uModel = new PropertyModel<String>(model, "units"); units = new TextField<String>("units", uModel); unitsContainer.add(units); IModel<String> usModel = new PropertyModel<String>(model, "unitSymbol"); unitSymbol = new TextField<String>("unitSymbol", usModel); unitsContainer.add(unitSymbol); // set defaults for elevation if units have never been set if ("elevation".equals(id) && uModel.getObject() == null) { uModel.setObject(DimensionInfo.ELEVATION_UNITS); usModel.setObject(DimensionInfo.ELEVATION_UNIT_SYMBOL); } // presentation/resolution block final WebMarkupContainer resContainer = new WebMarkupContainer("resolutionContainer"); resContainer.setOutputMarkupId(true); configs.add(resContainer); final WebMarkupContainer resolutions = new WebMarkupContainer("resolutions"); resolutions .setVisible(model.getObject().getPresentation() == DimensionPresentation.DISCRETE_INTERVAL); resolutions.setOutputMarkupId(true); resContainer.add(resolutions); presentationModes = new ArrayList<DimensionPresentation>(Arrays.asList(DimensionPresentation.values())); presentation = new DropDownChoice<DimensionPresentation>("presentation", new PropertyModel<DimensionPresentation>(model, "presentation"), presentationModes, new PresentationModeRenderer()); configs.add(presentation); presentation.setRequired(true); presentation.add(new AjaxFormComponentUpdatingBehavior("change") { @Override protected void onUpdate(AjaxRequestTarget target) { boolean visible = presentation.getModelObject() == DimensionPresentation.DISCRETE_INTERVAL; resolutions.setVisible(visible); target.add(resContainer); } }); IModel<BigDecimal> rmodel = new PropertyModel<BigDecimal>(model, "resolution"); resTime = new PeriodEditor("resTime", rmodel); resolutions.add(resTime); resElevation = new TextField<BigDecimal>("resElevation", rmodel); resolutions.add(resElevation); time = Date.class.isAssignableFrom(type); if(time) { resElevation.setVisible(false); resTime.setRequired(true); unitsContainer.setVisible(false); } else { resTime.setVisible(false); resElevation.setRequired(true); } //default value block DimensionDefaultValueSetting defValueSetting = model.getObject().getDefaultValue(); if (defValueSetting == null){ defValueSetting = new DimensionDefaultValueSetting(); model.getObject().setDefaultValue(defValueSetting); } final WebMarkupContainer defValueContainer = new WebMarkupContainer("defaultValueContainer"); defValueContainer.setOutputMarkupId(true); configs.add(defValueContainer); final WebMarkupContainer referenceValueContainer = new WebMarkupContainer("referenceValueContainer"); referenceValueContainer.setOutputMarkupId(true); referenceValueContainer.setVisible((defValueSetting.getStrategyType() == Strategy.FIXED) || (defValueSetting.getStrategyType() == Strategy.NEAREST)); defValueContainer.add(referenceValueContainer); defaultValueStrategies = new ArrayList<DimensionDefaultValueSetting.Strategy>(Arrays.asList(DimensionDefaultValueSetting.Strategy.values())); IModel<DimensionDefaultValueSetting.Strategy> strategyModel = new PropertyModel<DimensionDefaultValueSetting.Strategy>(model.getObject().getDefaultValue(), "strategy"); defaultValueStrategy = new DropDownChoice<DimensionDefaultValueSetting.Strategy>("strategy", strategyModel, defaultValueStrategies, new DefaultValueStrategyRenderer()); configs.add(defaultValueStrategy); defaultValueStrategy.add(new AjaxFormComponentUpdatingBehavior("change") { @Override protected void onUpdate(AjaxRequestTarget target) { boolean visible = (defaultValueStrategy.getModelObject() == Strategy.FIXED) || (defaultValueStrategy.getModelObject() == Strategy.NEAREST); referenceValueContainer.setVisible(visible); target.add(defValueContainer); } }); defValueContainer.add(defaultValueStrategy); final Label refValueValidationMessage = new Label("refValueValidationMsg", ""); refValueValidationMessage.setVisible(false); IModel<String> refValueModel = new PropertyModel<String>(model.getObject().getDefaultValue(), "referenceValue"); referenceValue = new TextField<String>("referenceValue", refValueModel); referenceValue.add(new AjaxFormComponentUpdatingBehavior("change") { protected void onUpdate(AjaxRequestTarget target) { refValueValidationMessage.setDefaultModelObject(null); refValueValidationMessage.setVisible(false); target.add(referenceValueContainer); } @Override protected void onError(AjaxRequestTarget target, RuntimeException e) { super.onError(target, e); if (referenceValue.hasErrorMessage()){ refValueValidationMessage.setDefaultModelObject(referenceValue.getFeedbackMessages().first()); refValueValidationMessage.setVisible(true); } target.add(referenceValueContainer); } }); referenceValue.add(new ReferenceValueValidator(id, strategyModel)); referenceValueContainer.add(referenceValue); referenceValueContainer.add(refValueValidationMessage); // set "current" for reference value if dimension is time, strategy is NEAREST and value has never been set if ("time".equals(id) && refValueModel.getObject() == null && strategyModel.getObject() == Strategy.NEAREST) { refValueModel.setObject(DimensionDefaultValueSetting.TIME_CURRENT); } } /** * Allows to remove presentation modes from the editor. If only a single presentation mode * is left the editor will setup in non enabled mode and will return that fixed value * @param mode */ public void disablePresentationMode(DimensionPresentation mode) { presentationModes.remove(mode); if(presentationModes.size() <= 1) { presentation.setModelObject(presentationModes.get(0)); presentation.setEnabled(false); } } private void disableDimension(Class<?> type, final WebMarkupContainer configs, Label noAttributeMessage) { // no attributes of the required type, no party enabled.setEnabled(false); enabled.setModelObject(false); configs.setVisible(false); ParamResourceModel typeName = new ParamResourceModel("AttributeType." + type.getSimpleName(), null); ParamResourceModel error = new ParamResourceModel("missingAttribute", this, typeName .getString()); noAttributeMessage.setDefaultModelObject(error.getString()); } @Override public boolean processChildren() { return true; } public void convertInput() { //Keep the original attributes if (!enabled.getModelObject()) { setConvertedInput(new DimensionInfoImpl()); } else { //To keep the original values for attributes not editable in UI: DimensionInfoImpl info = new DimensionInfoImpl(this.getModelObject()); info.setEnabled(true); attribute.processInput(); endAttribute.processInput(); info.setAttribute(attribute.getModelObject()); String endAttributeValue = endAttribute.getModelObject(); if ("-".equals(endAttributeValue)) { endAttributeValue = null; } info.setEndAttribute(endAttributeValue); units.processInput(); String unitsValue = units.getModelObject(); if ("time".equals(this.getId())) { // only one value is allowed for time units unitsValue = DimensionInfo.TIME_UNITS; } else if (unitsValue == null) { // allow blank units for any other dimension unitsValue = ""; } info.setUnits(unitsValue); unitSymbol.processInput(); info.setUnitSymbol(unitSymbol.getModelObject()); info.setPresentation(presentation.getModelObject()); if (info.getPresentation() == DimensionPresentation.DISCRETE_INTERVAL) { if(time) { resTime.processInput(); info.setResolution(resTime.getModelObject()); } else { resElevation.processInput(); info.setResolution(resElevation.getModelObject()); } } DimensionDefaultValueSetting defValueSetting = new DimensionDefaultValueSetting(); defaultValueStrategy.processInput(); defValueSetting.setStrategyType(defaultValueStrategy.getModelObject()); if (defValueSetting.getStrategyType() == Strategy.FIXED || defValueSetting.getStrategyType() == Strategy.NEAREST){ referenceValue.processInput(); if (referenceValue.hasErrorMessage()){ LOGGER.log(Level.SEVERE, "About to accept erroneous value "+referenceValue.getModelObject()); } defValueSetting.setReferenceValue(referenceValue.getModelObject()); } if (defValueSetting.getStrategyType() != Strategy.BUILTIN){ info.setDefaultValue(defValueSetting); } else { info.setDefaultValue(null); } setConvertedInput(info); } }; /** * Returns all attributes conforming to the specified type * * @param resource * @param type * */ List<String> getAttributesOfType(ResourceInfo resource, Class<?> type) { List<String> result = new ArrayList<String>(); if (resource instanceof FeatureTypeInfo) { try { FeatureTypeInfo ft = (FeatureTypeInfo) resource; for (PropertyDescriptor pd : ft.getFeatureType() .getDescriptors()) { if (type.isAssignableFrom(pd.getType().getBinding())) { result.add(pd.getName().getLocalPart()); } } } catch (IOException e) { throw new WicketRuntimeException(e); } } return result; } /** * Renders a presentation mode into a human readable form * * @author Alessio */ public class PresentationModeRenderer extends ChoiceRenderer<DimensionPresentation> { public PresentationModeRenderer() { super(); } public Object getDisplayValue(DimensionPresentation object) { return new ParamResourceModel(object.name(), DimensionEditor.this).getString(); } public String getIdValue(DimensionPresentation object, int index) { return String.valueOf(object.ordinal()); } } /** * Renders a default value strategy into a human readable form * * @author Ilkka Rinne / Spatineo Inc for the Finnish Meteorological Institute */ public class DefaultValueStrategyRenderer extends ChoiceRenderer<DimensionDefaultValueSetting.Strategy> { public DefaultValueStrategyRenderer() { super(); } public Object getDisplayValue(DimensionDefaultValueSetting.Strategy object) { return new ParamResourceModel(object.name(), DimensionEditor.this).getString(); } public String getIdValue(DimensionDefaultValueSetting.Strategy object, int index) { return String.valueOf(object.ordinal()); } } /** * Validator for dimension default value reference values. * * @author Ilkka Rinne / Spatineo Inc for the Finnish Meteorological Institute * */ public class ReferenceValueValidator implements IValidator<String> { String dimension; IModel<DimensionDefaultValueSetting.Strategy> strategyModel; public ReferenceValueValidator(String dimensionId, IModel<DimensionDefaultValueSetting.Strategy> strategyModel){ this.dimension = dimensionId; this.strategyModel = strategyModel; } @Override public void validate(IValidatable<String> value) { String stringValue = value.getValue(); if ( ((strategyModel.getObject() == Strategy.FIXED) || (strategyModel.getObject() == Strategy.NEAREST)) && stringValue == null){ value.error(new ValidationError("emptyReferenceValue").addKey("emptyReferenceValue")); } else if (dimension.equals("time")) { if(!isValidTimeReference(stringValue, strategyModel.getObject())) { String messageKey = strategyModel.getObject() == Strategy.NEAREST ? "invalidNearestTimeReferenceValue" : "invalidTimeReferenceValue"; value.error(new ValidationError(messageKey).addKey(messageKey)); } } else if (dimension.equals("elevation")) { if(!isValidElevationReference(stringValue)) { value.error(new ValidationError("invalidElevationReferenceValue") .addKey("invalidElevationReferenceValue")); } } } private boolean isValidElevationReference(String stringValue) { try { ElevationKvpParser parser = GeoServerExtensions.bean(ElevationKvpParser.class); List values = (List) parser.parse(stringValue); // the KVP parser accepts also lists of values, we want a single one return values.size() == 1; } catch (Exception e) { if(LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Invalid elevation value " + stringValue, e); } return false; } } private boolean isValidTimeReference(String stringValue, Strategy strategy) { try { TimeParser parser = new TimeParser(); List values = (List) parser.parse(stringValue); // the KVP parser accepts also lists of values, we want a single one if(strategy == Strategy.FIXED) { // point or range, but just one return values.size() == 1; } else if(strategy == Strategy.NEAREST) { // only point value, no ranges allowed return values.size() == 1 && !(values.get(0) instanceof Range); } else { // nope, we cannot have a reference value if the strategy is // not fixed or nearest return false; } } catch (Exception e) { if(LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Invalid time value " + stringValue, e); } return false; } } } }