/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package au.gov.ga.earthsci.core.temporal.timescale;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import au.gov.ga.earthsci.common.util.Range;
import au.gov.ga.earthsci.core.temporal.BigTime;
import au.gov.ga.earthsci.worldwind.common.util.Validate;
/**
* A basic (mostly) immutable implementation of the {@link ITimePeriod}
* interface that provides a builder mechanism for populating fields.
* <p/>
* Enforces the requirement that all sub-periods have a level greater than the
* level of this period.
* <p/>
* Provides the ability to plug in filters used to resolve overlapping
* sub-periods in the {@link #getSubPeriod(BigTime)} method. The default filter
* will return all sub-periods with a range that contains the given time.
* <p/>
* Additionally, label generator strategies can be provided to create labels for
* time instants that fall in this period. The default strategy is to return the
* name of the most specific period the time falls into.
* <p/>
* The only mutable field is the parent {@link ITimeScale}, which must be
* mutable to establish a back-reference to the time scale this period belongs
* to. This field is not intended to be set by client code.
* <p/>
* Equality is tested solely on the unique {@link #getId()}, and the natural
* order implemented in {@link #compareTo(ITimePeriod)} is based on the start of
* the period range, from earliest to latest.
*
* @author James Navin (james.navin@ga.gov.au)
*/
public class BasicTimePeriod implements ITimePeriod
{
/**
* An filter interface that can be used to resolve overlapping sub-periods
* for a specified time instant.
*/
public static interface SubPeriodFilter
{
/**
* Filter the provided list of candidate sub-periods to select
* appropriate sub-period(s) for the given time instant.
*/
List<ITimePeriod> filter(BigTime t, List<ITimePeriod> candidates);
}
/**
* A strategy interface for classes able to generate labels for a given time
* instant.
*/
public static interface LabelGenerator
{
/**
* Create and return a label for the given time instant in the given
* time period.
*/
String createLabel(BigTime t, ITimePeriod thisPeriod);
}
private static final SubPeriodFilter DEFAULT_SUB_PERIOD_FILTER = new SubPeriodFilter()
{
@Override
public List<ITimePeriod> filter(BigTime t, List<ITimePeriod> candidates)
{
return candidates;
}
};
private static final LabelGenerator DEFAULT_LABEL_GENERATOR = new LabelGenerator()
{
@Override
public String createLabel(BigTime t, ITimePeriod thisPeriod)
{
if (!thisPeriod.hasSubPeriods())
{
return thisPeriod.getName();
}
List<ITimePeriod> subPeriods = thisPeriod.getSubPeriod(t);
if (subPeriods == null || subPeriods.isEmpty())
{
return thisPeriod.getName();
}
StringBuffer buffer = new StringBuffer();
Iterator<ITimePeriod> subPeriodIt = subPeriods.iterator();
while (subPeriodIt.hasNext())
{
buffer.append(subPeriodIt.next().getLabel(t));
if (subPeriodIt.hasNext())
{
buffer.append(" / "); //$NON-NLS-1$
}
}
return buffer.toString();
}
};
private final SubPeriodFilter subPeriodFilter;
private final LabelGenerator labelGenerator;
private final String id;
private final String name;
private final String description;
private final ITimeScaleLevel level;
private final Range<BigTime> range;
private final List<ITimePeriod> subPeriods;
@SuppressWarnings("unchecked")
public BasicTimePeriod(String id, String name, String description, ITimeScaleLevel level, Range<BigTime> range,
List<ITimePeriod> subPeriods, SubPeriodFilter subPeriodFilter, LabelGenerator labelGenerator)
{
super();
this.id = id;
this.name = name;
this.description = description;
this.level = level;
this.range = range;
this.subPeriods =
(List<ITimePeriod>) (subPeriods == null ? Collections.emptyList() : Collections
.unmodifiableList(subPeriods));
this.subPeriodFilter = subPeriodFilter;
this.labelGenerator = labelGenerator;
}
@Override
public String getId()
{
return id;
}
@Override
public String getName()
{
return name;
}
@Override
public String getDescription()
{
return description;
}
@Override
public int compareTo(ITimePeriod o)
{
// Comparison performed on the start time of the period range
Range<BigTime> thisRange = getRange();
Range<BigTime> otherRange = o.getRange();
if (thisRange.isOpenLeft() && otherRange.isOpenLeft())
{
return 0;
}
else if (thisRange.isOpenLeft())
{
return -1;
}
else if (o.getRange().isOpenLeft())
{
return 1;
}
return thisRange.getMinValue().compareTo(o.getRange().getMinValue());
}
@Override
public boolean equals(Object obj)
{
if (obj == this)
{
return true;
}
if (!(obj instanceof ITimePeriod))
{
return false;
}
ITimePeriod other = (ITimePeriod) obj;
return id.equals(other.getId());
}
@Override
public int hashCode()
{
return id.hashCode();
}
@Override
public ITimeScaleLevel getLevel()
{
return level;
}
@Override
public boolean hasSubPeriods()
{
return !subPeriods.isEmpty();
}
@Override
public boolean hasSubPeriod(ITimePeriod p)
{
if (p == null)
{
return false;
}
for (ITimePeriod sub : subPeriods)
{
if (sub.equals(p) || sub.hasSubPeriod(p))
{
return true;
}
}
return false;
}
@Override
public List<ITimePeriod> getSubPeriods()
{
return subPeriods;
}
@Override
public List<ITimePeriod> getSubPeriod(BigTime t)
{
List<ITimePeriod> candidates = new ArrayList<ITimePeriod>();
for (ITimePeriod p : subPeriods)
{
if (p.contains(t))
{
candidates.add(p);
}
}
if (subPeriodFilter != null)
{
return subPeriodFilter.filter(t, candidates);
}
return DEFAULT_SUB_PERIOD_FILTER.filter(t, candidates);
}
@Override
public Range<BigTime> getRange()
{
return range;
}
@Override
public boolean contains(BigTime t)
{
return getRange().contains(t);
}
@Override
public String getLabel(BigTime t)
{
if (!contains(t))
{
return null;
}
if (labelGenerator != null)
{
return labelGenerator.createLabel(t, this);
}
return DEFAULT_LABEL_GENERATOR.createLabel(t, this);
}
/**
* A builder class for creating instances of {@link BasicTimePeriod}s from
* collected values.
*/
public static class Builder
{
private String id;
private String name;
private String description;
private ITimeScaleLevel level;
private Range<BigTime> range;
private List<ITimePeriod> subPeriods = new ArrayList<ITimePeriod>();
private SubPeriodFilter subPeriodFilter = null;
private LabelGenerator labelGenerator = null;
private Builder(String id, String name, String description)
{
this.id = id;
this.name = name;
this.description = description;
}
/**
* Create and return a new {@link Builder} instance that can be used to
* create a new {@link BasicTimePeriod}
*/
public static Builder buildTimePeriod(String id, String name, String description)
{
return new Builder(id, name, description);
}
public BasicTimePeriod build()
{
validate();
Collections.sort(subPeriods);
return new BasicTimePeriod(id, name, description, level, range == null ? new Range<BigTime>(null, null)
: range, Collections.unmodifiableList(subPeriods), subPeriodFilter, labelGenerator);
}
private void validate()
{
Validate.notBlank(id, "An ID is required"); //$NON-NLS-1$
Validate.notBlank(name, "A name is required"); //$NON-NLS-1$
Validate.notNull(level, "A level is required"); //$NON-NLS-1$
// Look for duplicate IDs in the sub-tree below this node.
// This is not the most efficient way to do this (would be better to validate
// once from the top-level periods), but the period structure is unlikely to be
// very deep, and so it shouldn't be too expensive.
Set<String> ids = new HashSet<String>();
ids.add(id);
int count = 1;
for (ITimePeriod sub : subPeriods)
{
if (sub.getLevel().compareTo(level) <= 0)
{
throw new IllegalArgumentException("Sub-periods must have a higher level than the parent period"); //$NON-NLS-1$
}
count += collectSubPeriodIds(sub, ids);
}
Validate.isTrue(ids.size() == count, "Cannot have duplicate IDs in period hierarchy"); //$NON-NLS-1$
}
private int collectSubPeriodIds(ITimePeriod p, Set<String> ids)
{
ids.add(p.getId());
int count = 1;
for (ITimePeriod sub : p.getSubPeriods())
{
count += collectSubPeriodIds(sub, ids);
}
return count;
}
/**
* Provide an (inclusive) range for the period. A <code>null</code>
* parameter will imply an open end to the range.
*/
public Builder withRange(BigTime start, BigTime end)
{
this.range = new Range<BigTime>(start, end);
return this;
}
/**
* Set the start date of the range of the period
*/
public Builder from(BigTime start, boolean inclusive)
{
if (this.range == null)
{
this.range = new Range<BigTime>(start, inclusive, null, true);
}
else
{
this.range = new Range<BigTime>(start, inclusive, range.getMaxValue(), range.isInclusiveRight());
}
return this;
}
/**
* Set the end date of the range of the period
*/
public Builder to(BigTime end, boolean inclusive)
{
if (this.range == null)
{
this.range = new Range<BigTime>(null, true, end, inclusive);
}
else
{
this.range = new Range<BigTime>(range.getMinValue(), range.isInclusiveLeft(), end, inclusive);
}
return this;
}
/**
* Add all provided sub-periods to the period
*/
public Builder withSubPeriods(ITimePeriod... subPeriods)
{
if (subPeriods == null)
{
return this;
}
this.subPeriods.addAll(Arrays.asList(subPeriods));
return this;
}
/**
* Add all provided sub-periods to the period
*/
public Builder withSubPeriods(Collection<ITimePeriod> subPeriods)
{
if (subPeriods == null)
{
return this;
}
this.subPeriods.addAll(subPeriods);
return this;
}
/**
* Add all provided sub-periods to the period
*/
public Builder withSubPeriod(ITimePeriod subPeriod)
{
if (subPeriod == null)
{
return this;
}
this.subPeriods.add(subPeriod);
return this;
}
/**
* Set a label generation strategy on the created period
*/
public Builder withLabelGenerator(LabelGenerator g)
{
this.labelGenerator = g;
return this;
}
/**
* Set a sub-period filter on the created period
*/
public Builder withSubPeriodFilter(SubPeriodFilter f)
{
this.subPeriodFilter = f;
return this;
}
/**
* Set the time scale level of this period
*/
public Builder atLevel(ITimeScaleLevel l)
{
this.level = l;
return this;
}
}
}