Skip to content

Commit

Permalink
[GEOS-8596] WMS-T "nearest match" support for time dimension (#2764)
Browse files Browse the repository at this point in the history
[GEOS-8596] WMS-T "nearest match" support for time dimension
  • Loading branch information
aaime committed Feb 23, 2018
1 parent 1384ed5 commit f85a5f1
Show file tree
Hide file tree
Showing 29 changed files with 1,820 additions and 120 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions doc/en/user/source/data/webadmin/layers.rst
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ For each enabled dimension the following configuration options are available:
* **reference value**—Tries to use the given reference value as-is, regardless of whether its actually available in the data or not. * **reference value**—Tries to use the given reference value as-is, regardless of whether its actually available in the data or not.


* **Reference value**—The default value specifier. Only shown for the default value strategies where its used. * **Reference value**—The default value specifier. Only shown for the default value strategies where its used.
* **Nearest match**—Whether to enable, or not, WMS nearest match support on this dimension. Currently supported only on the time dimension.
* **Acceptable interval**—A maximum search distance from the specified value (available only when nearest match is enabled).
Can be empty (no limit), a single value (symmetric search) or using a ``before/after`` syntax to
specify an asymmetric search range. Time distances should specified using the ISO period syntax. For example, ``PT1H/PT0H`` allows to search up to one hour before the user specified value,
but not after.


For time dimension the value must be in ISO 8601 DateTime format ``yyyy-MM-ddThh:mm:ss.SSSZ`` For elevation dimension, the value must be and integer of floating point number. For time dimension the value must be in ISO 8601 DateTime format ``yyyy-MM-ddThh:mm:ss.SSSZ`` For elevation dimension, the value must be and integer of floating point number.


Expand Down
118 changes: 118 additions & 0 deletions src/main/src/main/java/org/geoserver/catalog/AcceptableRange.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,118 @@
/* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.catalog;

import org.geoserver.ows.kvp.TimeParser;
import org.geotools.util.DateRange;
import org.geotools.util.Range;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;

/**
* Represents the parsed acceptable range. For elevation it's simple numbers, for dates it's a number of milliseconds.
*/
public class AcceptableRange {

/**
* Parses the acceptable range
*
* @param spec The specification from the UI
* @param dataType The target data type (e.g. {@link Date}
* @return An {@link AcceptableRange} object, or null if the spec was null or empty
*/
public static AcceptableRange getAcceptableRange(String spec, Class dataType) throws ParseException {
if (spec == null || spec.trim().isEmpty()) {
return null;
}

String[] split = spec.split("/");
if (split.length > 2) {
throw new IllegalArgumentException("Invalid acceptable range specification, must be either a single " +
"value, or two values split by a forward slash");
}
Number before = parseValue(split[0], dataType);
Number after = before;
if (split.length == 2) {
after = parseValue(split[1], dataType);
}
// avoid complications in case the search range is empty
if (before.doubleValue() == 0 && after.doubleValue() == 0) {
return null;
}
return new AcceptableRange(before, after, dataType);
}

private static Number parseValue(String s, Class dataType) throws ParseException {
if (Date.class.isAssignableFrom(dataType)) {
return TimeParser.parsePeriod(s);
}
// TODO: add support for Number, e.g., elevation
throw new IllegalArgumentException("Unsupported value type " + dataType);
}

private Number before;
private Number after;
private Class dataType;

public AcceptableRange(Number before, Number after, Class dataType) {
this.before = before;
this.after = after;
this.dataType = dataType;
}

public Range getSearchRange(Object value) {
if (value instanceof Range) {
Range range = (Range) value;
Range before = getSearchRangeOnSingleValue(range.getMinValue());
Range after = getSearchRangeOnSingleValue(range.getMaxValue());
return before.union(after);
} else {
return getSearchRangeOnSingleValue(value);
}

}

public Range getSearchRangeOnSingleValue(Object value) {
if (Date.class.isAssignableFrom(dataType)) {
Date center = (Date) value;
Calendar cal = Calendar.getInstance();
cal.setTime(center);
cal.setTimeInMillis(cal.getTimeInMillis() - before.longValue());
Date min = cal.getTime();
cal.setTime(center);
cal.setTimeInMillis(cal.getTimeInMillis() + after.longValue());
Date max = cal.getTime();
return new DateRange(min, max);
}
// TODO: add support for Number, e.g., elevation
throw new IllegalArgumentException("Unsupported value type " + dataType);
}

/**
* Before offset
* @return
*/
public Number getBefore() {
return before;
}

/**
* After offset
* @return
*/
public Number getAfter() {
return after;
}

/**
* The range data type
* @return
*/
public Class getDataType() {
return dataType;
}
}
27 changes: 27 additions & 0 deletions src/main/src/main/java/org/geoserver/catalog/DimensionInfo.java
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -100,4 +100,31 @@ public interface DimensionInfo extends Serializable {
public DimensionDefaultValueSetting getDefaultValue(); public DimensionDefaultValueSetting getDefaultValue();


public void setDefaultValue(DimensionDefaultValueSetting defaultValue); public void setDefaultValue(DimensionDefaultValueSetting defaultValue);

/**
* Returns true if the nearest match behavior is implemented. Right now it's only available for the TIME
* dimension, support for other dimensions might come later
*/
public boolean isNearestMatchEnabled();

/**
* Enables/disables nearest match.
* @param nearestMatch
*/
public void setNearestMatchEnabled(boolean nearestMatch);

/**
* Returns a string specifying the search range. Can be empty, a single value (to be parsed in the data
* type of the dimension, in particular, it will be a ISO period for times) or a {code}before/after{code} range
* specifying how far to search from the requested value (e.g., {code}PT12H/PT1H{code} to allow searching 12 hours
* in the past but only 1 hour in the future).
* @return
*/
public String getAcceptableInterval();

/**
* Allows setting the search range for nearest matches, see also {@link #getAcceptableInterval()}.
* @param acceptableInterval
*/
public void setAcceptableInterval(String acceptableInterval);
} }
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public interface ResourceInfo extends CatalogInfo {
* The time dimension * The time dimension
*/ */
static final String TIME = "time"; static final String TIME = "time";

/**
* The default time unit
*/
static final String TIME_UNIT = "ISO8601";


/** /**
* The elevation dimension * The elevation dimension
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.geoserver.catalog.impl; package org.geoserver.catalog.impl;


import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Objects;


import org.geoserver.catalog.DimensionDefaultValueSetting; import org.geoserver.catalog.DimensionDefaultValueSetting;
import org.geoserver.catalog.DimensionInfo; import org.geoserver.catalog.DimensionInfo;
Expand Down Expand Up @@ -39,6 +40,10 @@ public class DimensionInfoImpl implements DimensionInfo {


DimensionDefaultValueSetting defaultValue; DimensionDefaultValueSetting defaultValue;


Boolean nearestMatchEnabled;

String acceptableInterval;

/** /**
* The default constructor * The default constructor
*/ */
Expand Down Expand Up @@ -118,7 +123,26 @@ public String getUnitSymbol() {
public void setUnitSymbol(String unitSymbol) { public void setUnitSymbol(String unitSymbol) {
this.unitSymbol = unitSymbol; this.unitSymbol = unitSymbol;
} }


public boolean isNearestMatchEnabled() {
// for backwards compatiblity we allow nearest search to be null
return nearestMatchEnabled == null ? false : nearestMatchEnabled;
}

public void setNearestMatchEnabled(boolean nearestMatchEnabled) {
this.nearestMatchEnabled = nearestMatchEnabled;
}

@Override
public String getAcceptableInterval() {
return acceptableInterval;
}

@Override
public void setAcceptableInterval(String searchRange) {
this.acceptableInterval = searchRange;
}

@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
Expand All @@ -128,7 +152,10 @@ public String toString() {
sb.append(", units=").append(units); sb.append(", units=").append(units);
sb.append(", unitSymbol=").append(unitSymbol); sb.append(", unitSymbol=").append(unitSymbol);
sb.append(", presentation=").append(presentation); sb.append(", presentation=").append(presentation);
sb.append(", resolution=").append(resolution).append("]"); sb.append(", resolution=").append(resolution);
sb.append(", nearest=").append(nearestMatchEnabled);
sb.append(", acceptableInterval=").append(acceptableInterval);
sb.append("]");
return sb.toString(); return sb.toString();
} }


Expand All @@ -143,51 +170,26 @@ public int hashCode() {
result = prime * result + ((unitSymbol == null) ? 0 : unitSymbol.hashCode()); result = prime * result + ((unitSymbol == null) ? 0 : unitSymbol.hashCode());
result = prime * result + ((presentation == null) ? 0 : presentation.hashCode()); result = prime * result + ((presentation == null) ? 0 : presentation.hashCode());
result = prime * result + ((resolution == null) ? 0 : resolution.hashCode()); result = prime * result + ((resolution == null) ? 0 : resolution.hashCode());
result = prime * result + ((nearestMatchEnabled == null) ? 0 : resolution.hashCode());
result = prime * result + ((acceptableInterval == null) ? 0 : resolution.hashCode());
return result; return result;
} }


@Override @Override
public boolean equals(Object obj) { public boolean equals(Object o) {
if (this == obj) if (this == o) return true;
return true; if (o == null || getClass() != o.getClass()) return false;
if (obj == null) DimensionInfoImpl that = (DimensionInfoImpl) o;
return false; return enabled == that.enabled &&
if (getClass() != obj.getClass()) Objects.equals(attribute, that.attribute) &&
return false; Objects.equals(endAttribute, that.endAttribute) &&
DimensionInfoImpl other = (DimensionInfoImpl) obj; presentation == that.presentation &&
if (attribute == null) { Objects.equals(resolution, that.resolution) &&
if (other.attribute != null) Objects.equals(units, that.units) &&
return false; Objects.equals(unitSymbol, that.unitSymbol) &&
} else if (!attribute.equals(other.attribute)) Objects.equals(defaultValue, that.defaultValue) &&
return false; Objects.equals(nearestMatchEnabled, that.nearestMatchEnabled) &&
if (units == null) { Objects.equals(acceptableInterval, that.acceptableInterval);
if (other.units != null)
return false;
} else if (!units.equals(other.units))
return false;
if (unitSymbol == null) {
if (other.unitSymbol != null)
return false;
} else if (!unitSymbol.equals(other.unitSymbol))
return false;
if (enabled != other.enabled)
return false;
if (presentation == null) {
if (other.presentation != null)
return false;
} else if (!presentation.equals(other.presentation))
return false;
if (resolution == null) {
if (other.resolution != null)
return false;
} else if (!resolution.equals(other.resolution))
return false;
if (endAttribute == null) {
if (other.endAttribute != null)
return false;
} else if (!endAttribute.equals(other.endAttribute))
return false;
return true;
} }


@Override @Override
Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,53 @@
/* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.catalog;

import org.geotools.util.DateRange;
import org.junit.Test;

import java.util.Date;

import static junit.framework.TestCase.assertEquals;

public class AcceptableRangeTest {

public static final long DAY_IN_MS = 1000 * 60 * 60 * 24;

@Test
public void testSymmetricTimeRange() throws Exception {
AcceptableRange range = AcceptableRange.getAcceptableRange("P1D", Date.class);
assertEquals(DAY_IN_MS, range.getBefore());
assertEquals(DAY_IN_MS, range.getAfter());

Date value = new Date();
DateRange searchRange = (DateRange) range.getSearchRange(value);
assertEquals(DAY_IN_MS, value.getTime() - searchRange.getMinValue().getTime());
assertEquals(DAY_IN_MS, searchRange.getMaxValue().getTime() - value.getTime());
}

@Test
public void testPastTimeRange() throws Exception {
AcceptableRange range = AcceptableRange.getAcceptableRange("P1D/P0D", Date.class);
assertEquals(DAY_IN_MS, range.getBefore());
assertEquals(0l, range.getAfter());

Date value = new Date();
DateRange searchRange = (DateRange) range.getSearchRange(value);
assertEquals(DAY_IN_MS, value.getTime() - searchRange.getMinValue().getTime());
assertEquals(0l, searchRange.getMaxValue().getTime() - value.getTime());
}

@Test
public void testFutureTimeRange() throws Exception {
AcceptableRange range = AcceptableRange.getAcceptableRange("P0D/P1D", Date.class);
assertEquals(0l, range.getBefore());
assertEquals(DAY_IN_MS, range.getAfter());

Date value = new Date();
DateRange searchRange = (DateRange) range.getSearchRange(value);
assertEquals(0l, value.getTime() - searchRange.getMinValue().getTime());
assertEquals(DAY_IN_MS, searchRange.getMaxValue().getTime() - value.getTime());
}
}

0 comments on commit f85a5f1

Please sign in to comment.