Issue #2070 Add geospatial constraints to metar precip requests.

Change-Id: Ie7ce8ac0f3d2655c5e427aee0588b8546b1674d1

Former-commit-id: 3a6af1b023 [formerly 984410e172 [formerly b94373379d] [formerly 3a6af1b023 [formerly fe7cfca3760374fdeac084158ddb6f1b1a4e5d54]]]
Former-commit-id: 984410e172 [formerly b94373379d]
Former-commit-id: 984410e172
Former-commit-id: c9b6c18cab
This commit is contained in:
Ben Steffensmeier 2013-06-07 12:40:48 -05:00
parent 6632420d11
commit 2acab55876
2 changed files with 328 additions and 74 deletions

View file

@ -37,21 +37,30 @@ import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.graphics.RGB;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.Envelope2D;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import com.raytheon.uf.common.dataplugin.PluginDataObject;
import com.raytheon.uf.common.dataplugin.annotations.DataURIUtil;
import com.raytheon.uf.common.dataquery.requests.RequestConstraint;
import com.raytheon.uf.common.dataquery.requests.RequestConstraint.ConstraintType;
import com.raytheon.uf.common.geospatial.MapUtil;
import com.raytheon.uf.common.geospatial.ReferencedCoordinate;
import com.raytheon.uf.common.status.IUFStatusHandler;
import com.raytheon.uf.common.status.UFStatus;
import com.raytheon.uf.common.status.UFStatus.Priority;
import com.raytheon.uf.common.time.DataTime;
import com.raytheon.uf.viz.core.DrawableString;
import com.raytheon.uf.viz.core.IExtent;
import com.raytheon.uf.viz.core.IGraphicsTarget;
import com.raytheon.uf.viz.core.IGraphicsTarget.HorizontalAlignment;
import com.raytheon.uf.viz.core.IGraphicsTarget.VerticalAlignment;
import com.raytheon.uf.viz.core.RecordFactory;
import com.raytheon.uf.viz.core.drawables.IDescriptor.FramesInfo;
import com.raytheon.uf.viz.core.drawables.IFont;
import com.raytheon.uf.viz.core.drawables.IFont.Style;
@ -60,7 +69,7 @@ import com.raytheon.uf.viz.core.drawables.PaintProperties;
import com.raytheon.uf.viz.core.exception.VizException;
import com.raytheon.uf.viz.core.map.IMapDescriptor;
import com.raytheon.uf.viz.core.rsc.AbstractVizResource;
import com.raytheon.uf.viz.core.rsc.IResourceDataChanged;
import com.raytheon.uf.viz.core.rsc.IResourceDataChanged.ChangeType;
import com.raytheon.uf.viz.core.rsc.LoadProperties;
import com.raytheon.uf.viz.core.rsc.capabilities.ColorableCapability;
import com.raytheon.uf.viz.core.rsc.capabilities.DensityCapability;
@ -71,7 +80,10 @@ import com.vividsolutions.jts.geom.Coordinate;
/**
*
* TODO Add Description
* Resource for displaying Metar Precip values as strings on the map. Uses
* custom data request so that it can use derived precip values. Uses custom
* progressive disclosure so that sites with the most precip are always
* disclosed first.
*
* <pre>
*
@ -79,7 +91,9 @@ import com.vividsolutions.jts.geom.Coordinate;
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Aug 19, 2011 bsteffen Initial creation
* Aug 19, 2011 bsteffen Initial creation
* Jun 07, 2013 2070 bsteffen Add geospatial constraints to metar
* precip requests.
*
* </pre>
*
@ -88,6 +102,8 @@ import com.vividsolutions.jts.geom.Coordinate;
*/
public class MetarPrecipResource extends
AbstractVizResource<MetarPrecipResourceData, IMapDescriptor> {
private static final transient IUFStatusHandler statusHandler = UFStatus
.getHandler(MetarPrecipResource.class);
private static final int PLOT_PIXEL_SIZE = 30;
@ -120,10 +136,6 @@ public class MetarPrecipResource extends
return Status.CANCEL_STATUS;
}
processRemoves();
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
processReproject();
return Status.OK_STATUS;
}
@ -195,7 +207,7 @@ public class MetarPrecipResource extends
continue;
}
if (data.distValue >= threshold) {
// This is easier then changing it when the capability cahnges.
// This is easier then changing it when the capability changes.
data.string.font = this.font;
data.string.setText(data.string.getText(), color);
strings.add(data.string);
@ -220,32 +232,30 @@ public class MetarPrecipResource extends
@Override
protected void initInternal(IGraphicsTarget target) throws VizException {
dataTimes = new ArrayList<DataTime>();
dataProcessJob.schedule();
resourceData.addChangeListener(new IResourceDataChanged() {
}
@Override
public void resourceChanged(ChangeType type, Object object) {
if (type == ChangeType.CAPABILITY) {
if (object instanceof MagnificationCapability) {
if (font != null) {
font.dispose();
font = null;
}
}
issueRefresh();
} else if (type == ChangeType.DATA_UPDATE) {
if (object instanceof PluginDataObject[]) {
PluginDataObject[] pdos = (PluginDataObject[]) object;
for (PluginDataObject pdo : pdos) {
updates.offer(pdo);
dataProcessJob.schedule();
}
}
@Override
protected void resourceDataChanged(ChangeType type, Object object) {
super.resourceDataChanged(type, object);
if (type == ChangeType.CAPABILITY) {
if (object instanceof MagnificationCapability) {
if (font != null) {
font.dispose();
font = null;
}
}
});
issueRefresh();
} else if (type == ChangeType.DATA_UPDATE) {
if (object instanceof PluginDataObject[]) {
PluginDataObject[] pdos = (PluginDataObject[]) object;
for (PluginDataObject pdo : pdos) {
updates.offer(pdo);
dataProcessJob.schedule();
}
}
}
}
@Override
@ -255,12 +265,14 @@ public class MetarPrecipResource extends
@Override
public void remove(DataTime dataTime) {
// This will be handled asynchronously by the update job
removes.offer(dataTime);
dataProcessJob.schedule();
}
@Override
public void project(CoordinateReferenceSystem crs) throws VizException {
// This will be handled asynchronously by the update job
reproject = true;
dataProcessJob.schedule();
}
@ -313,21 +325,35 @@ public class MetarPrecipResource extends
return "No Data";
}
private void processReproject() {
private boolean processReproject() {
if (reproject) {
reproject = false;
// reproject all stations to the new crs and throw out any off the
// screen
GridEnvelope2D envelope = GridGeometry2D.wrap(
descriptor.getGridGeometry()).getGridRange2D();
synchronized (data) {
for (List<RenderablePrecipData> dataList : data.values()) {
for (RenderablePrecipData precip : dataList) {
Iterator<RenderablePrecipData> it = dataList.iterator();
while (it.hasNext()) {
RenderablePrecipData precip = it.next();
Coordinate latLon = precip.getLatLon();
double[] px = descriptor.worldToPixel(new double[] {
latLon.x, latLon.y });
precip.string.setCoordinates(px[0], px[1], px[2]);
if (envelope.contains(px[0], px[1])) {
precip.string.setCoordinates(px[0], px[1], px[2]);
} else {
it.remove();
}
}
}
}
// returning true will tell the caller to reload the frame in case
// any data in the new area was outside the old area
return true;
}
issueRefresh();
return false;
}
private void processRemoves() {
@ -350,20 +376,48 @@ public class MetarPrecipResource extends
RequestConstraint rc = new RequestConstraint(null, ConstraintType.IN);
long earliestTime = Long.MAX_VALUE;
Set<String> newStations = new HashSet<String>();
// Get the envelope and math transform to ensure we only bother
// processing updates on screen.
MathTransform toDescriptor = null;
try {
toDescriptor = MapUtil.getTransformFromLatLon(descriptor.getCRS());
} catch (FactoryException e) {
statusHandler
.handle(Priority.PROBLEM,
"Error processing updates for MetarPrecip, Ignoring all updates.",
e);
updates.clear();
return;
}
Envelope2D envelope = new Envelope2D(descriptor.getGridGeometry()
.getEnvelope());
while (!updates.isEmpty()) {
PluginDataObject pdo = updates.poll();
try {
Map<String, Object> map = RecordFactory.getInstance()
.loadMapFromUri(pdo.getDataURI());
newStations.add(map.get("location.stationId").toString());
} catch (VizException e) {
throw new RuntimeException(e);
}
long validTime = pdo.getDataTime().getMatchValid();
if (validTime < earliestTime) {
earliestTime = validTime;
Map<String, Object> map = DataURIUtil.createDataURIMap(pdo);
double lon = ((Number) map.get("location.longitude"))
.doubleValue();
double lat = ((Number) map.get("location.latitude"))
.doubleValue();
DirectPosition2D dp = new DirectPosition2D(lon, lat);
toDescriptor.transform(dp, dp);
if (envelope.contains(dp)) {
newStations.add(map.get("location.stationId").toString());
long validTime = pdo.getDataTime().getMatchValid();
if (validTime < earliestTime) {
earliestTime = validTime;
}
}
} catch (Exception e) {
statusHandler
.handle(Priority.PROBLEM,
"Error processing updates for MetarPrecip, Ignoring an update.",
e);
}
}
if (newStations.isEmpty()) {
return;
}
rc.setConstraintValueList(newStations.toArray(new String[0]));
rcMap.put("location.stationId", rc);
MetarPrecipDataContainer container = new MetarPrecipDataContainer(
@ -393,16 +447,25 @@ public class MetarPrecipResource extends
}
private void processNewFrames(IProgressMonitor monitor) {
// load data in two steps, first load base data then any derived data.
// Always try to load the current frame, then nearby frames.
MetarPrecipDataContainer container = new MetarPrecipDataContainer(
resourceData.getDuration(), resourceData.getMetadataMap());
resourceData.getDuration(), resourceData.getMetadataMap(),
descriptor.getGridGeometry().getEnvelope());
Set<DataTime> reprojectedFrames = new HashSet<DataTime>();
Set<DataTime> baseOnly = new HashSet<DataTime>();
boolean modified = true;
while (modified) {
// don't want to mis a reproject if retrieval takes awhile.
processReproject();
// don't want to miss a reproject if retrieval takes awhile.
if (processReproject()) {
// We must create a new container and re request all the data
// for the new area.
reprojectedFrames = new HashSet<DataTime>(data.keySet());
container = new MetarPrecipDataContainer(
resourceData.getDuration(),
resourceData.getMetadataMap(), descriptor
.getGridGeometry().getEnvelope());
}
if (monitor.isCanceled()) {
return;
}
@ -417,26 +480,25 @@ public class MetarPrecipResource extends
}
int curIndex = frameInfo.getFrameIndex();
int count = frameInfo.getFrameCount();
if (times.length != count) {
System.out.println("Uh oh");
}
// This will generate the number series 0, -1, 1, -2, 2, -3, 3...
for (int i = 0; i < count / 2 + 1; i = i < 0 ? -i : -i - 1) {
int index = (count + curIndex + i) % count;
DataTime next = times[index];
if (next != null) {
if (!data.containsKey(next)) {
if (!data.containsKey(next)
|| reprojectedFrames.contains(next)) {
List<PrecipData> baseData = container
.getBasePrecipData(next);
addData(next, baseData);
baseOnly.add(next);
reprojectedFrames.remove(next);
modified = true;
break;
}
if (baseOnly.contains(next)) {
List<PrecipData> baseData = container
List<PrecipData> derivedData = container
.getDerivedPrecipData(next);
addData(next, baseData);
addData(next, derivedData);
baseOnly.remove(next);
modified = true;
break;
@ -484,6 +546,9 @@ public class MetarPrecipResource extends
RGB color = getCapability(ColorableCapability.class).getColor();
GridEnvelope2D envelope = GridGeometry2D.wrap(
descriptor.getGridGeometry()).getGridRange2D();
for (int i = 0; i < precips.size(); i++) {
PrecipData precip = precips.get(i);
RenderablePrecipData data = null;
@ -493,6 +558,9 @@ public class MetarPrecipResource extends
data = new RenderablePrecipData(precip);
double[] px = descriptor.worldToPixel(new double[] {
precip.getLatLon().x, precip.getLatLon().y });
if (!envelope.contains(px[0], px[1])) {
continue;
}
data.string = new DrawableString(formatPrecip(precips.get(i)
.getPrecipAmt()), color);
data.string.setCoordinates(px[0], px[1], px[2]);
@ -509,7 +577,10 @@ public class MetarPrecipResource extends
}
}
data.distValue = bestDist;
newPrecips.add(data);
// this checks removes duplicates
if (bestDist > 0) {
newPrecips.add(data);
}
}
synchronized (data) {
data.put(time, newPrecips);

View file

@ -29,8 +29,12 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.geotools.geometry.jts.ReferencedEnvelope;
import com.raytheon.uf.common.dataquery.requests.RequestConstraint;
import com.raytheon.uf.common.dataquery.requests.RequestConstraint.ConstraintType;
import com.raytheon.uf.common.geospatial.MapUtil;
import com.raytheon.uf.common.geospatial.util.EnvelopeIntersection;
import com.raytheon.uf.common.pointdata.PointDataContainer;
import com.raytheon.uf.common.pointdata.PointDataView;
import com.raytheon.uf.common.status.IUFStatusHandler;
@ -40,10 +44,18 @@ import com.raytheon.uf.common.time.DataTime;
import com.raytheon.uf.viz.core.datastructure.DataCubeContainer;
import com.raytheon.uf.viz.core.exception.VizException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
/**
*
* TODO Add Description
* Container for requesting and caching metar precip data. This container can be
* reused to request data for multiple times. Typically it is used in 2 stages,
* first getBasePrecipData is used to quickly retrieve data for all metar
* stations which have the data directly available, second getDerivedPrecipData
* is used to get all the data taht is not directly available but has to be
* derived by accumulating other reports over time.
*
* <pre>
*
@ -51,8 +63,11 @@ import com.vividsolutions.jts.geom.Coordinate;
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Aug 19, 2011 bsteffen Initial creation
* Jan 10, 2013 snaples updated getBasePrecipData to use correct data for 1 hour precip.
* Aug 19, 2011 bsteffen Initial creation
* Jan 10, 2013 snaples updated getBasePrecipData to use correct
* data for 1 hour precip.
* Jun 07, 2013 2070 bsteffen Add geospatial constraints to metar
* precip requests.
*
* </pre>
*
@ -65,6 +80,13 @@ public class MetarPrecipDataContainer {
private static final transient IUFStatusHandler statusHandler = UFStatus
.getHandler(MetarPrecipDataContainer.class);
/*
* Envelope which contains the whole world in LatLon projection. Used for
* intersections/conversion since this is the valid area for metar records.
*/
private static final ReferencedEnvelope WORLD_LAT_LON_ENVELOPE = new ReferencedEnvelope(
-180, 180, -90, 90, MapUtil.LATLON_PROJECTION);
public static class PrecipData {
private final long timeObs;
@ -128,7 +150,11 @@ public class MetarPrecipDataContainer {
private final int duration;
private Map<String, RequestConstraint> rcMap = null;
private final Map<String, RequestConstraint> rcMap;
private final org.opengis.geometry.Envelope descriptorEnvelope;
private List<Envelope> latLonEnvelopes;
private final Map<Long, Map<String, PrecipData>> cache3 = new HashMap<Long, Map<String, PrecipData>>();
@ -136,12 +162,45 @@ public class MetarPrecipDataContainer {
private final Map<DataTime, Set<String>> baseStations = new HashMap<DataTime, Set<String>>();
/**
* Consturct a container with geospatially filtering to only request data in
* the area of descriptorEnvelope
*
* @param duration
* @param rcMap
* @param descriptorEnvelope
*/
public MetarPrecipDataContainer(int duration,
HashMap<String, RequestConstraint> rcMap,
org.opengis.geometry.Envelope descriptorEnvelope) {
this.duration = duration;
this.rcMap = rcMap;
this.descriptorEnvelope = descriptorEnvelope;
}
/**
* This will construct a container with no geospatial constraints, use it
* only for updates where the rcMap already has stationIds that are
* geospatially filtered.
*
* @param duration
* @param rcMap
*/
public MetarPrecipDataContainer(int duration,
Map<String, RequestConstraint> rcMap) {
this.duration = duration;
this.rcMap = rcMap;
this.descriptorEnvelope = null;
this.latLonEnvelopes = Arrays.<Envelope> asList(WORLD_LAT_LON_ENVELOPE);
}
/**
* Get the base precip data from all metar stations that have precip for the
* specified duration directly encoded.
*
* @param time
* @return
*/
public List<PrecipData> getBasePrecipData(DataTime time) {
Map<String, PrecipData> precipMap = new HashMap<String, PrecipData>();
long validTime = time.getMatchValid();
@ -150,12 +209,11 @@ public class MetarPrecipDataContainer {
P1_KEY);
Map<String, PrecipData> precipMap1 = null;
if (pdc != null) {
precipMap1 = createPrecipData(pdc,
validTime - ONE_HOUR, validTime, P1_KEY);
precipMap1 = createPrecipData(pdc, validTime - ONE_HOUR,
validTime, P1_KEY);
if (precipMap1 == null) {
precipMap1 = createPrecipData(pdc,
validTime - ONE_HOUR + FIFTEEN_MIN, validTime
- FIFTEEN_MIN, P1_KEY);
precipMap1 = createPrecipData(pdc, validTime - ONE_HOUR
+ FIFTEEN_MIN, validTime - FIFTEEN_MIN, P1_KEY);
}
// Data frame 15 minutes ago is better then data now for some
// reason
@ -183,6 +241,13 @@ public class MetarPrecipDataContainer {
return result;
}
/**
* Get the derived precip data for all stations which don't have the base
* data. This will attempt to accumulate precipitation from old reports.
*
* @param time
* @return
*/
public List<PrecipData> getDerivedPrecipData(DataTime time) {
Map<String, PrecipData> precipMap = new HashMap<String, PrecipData>();
long validTime = time.getMatchValid();
@ -217,6 +282,15 @@ public class MetarPrecipDataContainer {
return result;
}
/**
* Get raw metar data by summiong up multiple 1 hour observations
*
* @param validTime
* the valid time to get data for
* @param sumTime
* how many 1hour precip obs to sum up.
* @return
*/
private Map<String, PrecipData> getRawPrecipData1sum(long validTime,
int sumTime) {
List<Map<String, PrecipData>> maps = new ArrayList<Map<String, PrecipData>>();
@ -237,6 +311,12 @@ public class MetarPrecipDataContainer {
return add(maps.toArray(new Map[0]));
}
/**
* Get all base 3 hour precip records for the provided times
*
* @param validTime
* @return
*/
private Map<String, PrecipData> getRawPrecipData3(long validTime) {
if (cache3.containsKey(validTime)) {
return cache3.get(validTime);
@ -257,6 +337,12 @@ public class MetarPrecipDataContainer {
return precipMap3;
}
/**
* Get all base 6 hour precip records for the provided times
*
* @param validTime
* @return
*/
private Map<String, PrecipData> getRawPrecipData6(long validTime) {
if (cache6.containsKey(validTime)) {
return cache6.get(validTime);
@ -272,6 +358,16 @@ public class MetarPrecipDataContainer {
return precipMap6;
}
/**
* build PricipData objects for every station in a PointDataContainer
*
* @param pdc
* @param startTime
* @param latestTime
* @param precipKey
* the name of the parameter with precip.
* @return
*/
private Map<String, PrecipData> createPrecipData(PointDataContainer pdc,
long startTime, long latestTime, String precipKey) {
Map<String, PrecipData> precipMap = new HashMap<String, PrecipData>();
@ -307,6 +403,15 @@ public class MetarPrecipDataContainer {
return precipMap;
}
/**
* This function perfroms the request to edex for point data.
*
* @param rcMap
* @param time
* @param duration
* @param precipKeys
* @return
*/
private PointDataContainer requestPointData(
Map<String, RequestConstraint> rcMap, long time, int duration,
String... precipKeys) {
@ -331,21 +436,84 @@ public class MetarPrecipDataContainer {
end.toString() });
rcMap.put("dataTime", timeRC);
PointDataContainer pdc = null;
try {
pdc = DataCubeContainer.getPointData("obs",
parameters.toArray(new String[0]), rcMap);
} catch (VizException e) {
statusHandler.handle(Priority.ERROR,
"Error getting precip data, some precip will not display.",
e);
// Over the dateline there might be an envelope on either side.
for (Envelope latLonEnvelope : getLatLonEnvelopes()) {
PointDataContainer tmppdc = null;
RequestConstraint lonRC = new RequestConstraint(null,
ConstraintType.BETWEEN);
Double minLon = latLonEnvelope.getMinX();
Double maxLon = latLonEnvelope.getMaxX();
lonRC.setBetweenValueList(new String[] { minLon.toString(),
maxLon.toString() });
rcMap.put("location.longitude", lonRC);
RequestConstraint latRC = new RequestConstraint(null,
ConstraintType.BETWEEN);
Double minLat = latLonEnvelope.getMinY();
Double maxLat = latLonEnvelope.getMaxY();
latRC.setBetweenValueList(new String[] { minLat.toString(),
maxLat.toString() });
rcMap.put("location.latitude", latRC);
try {
tmppdc = DataCubeContainer.getPointData("obs",
parameters.toArray(new String[0]), rcMap);
} catch (VizException e) {
statusHandler
.handle(Priority.ERROR,
"Error getting precip data, some precip will not display.",
e);
}
if (tmppdc != null) {
tmppdc.setCurrentSz(tmppdc.getAllocatedSz());
if (pdc != null) {
pdc.combine(tmppdc);
pdc.setCurrentSz(pdc.getAllocatedSz());
} else {
pdc = tmppdc;
}
}
}
if (pdc != null) {
pdc.setCurrentSz(pdc.getAllocatedSz());
return pdc;
}
return null;
return pdc;
}
/**
* Get envelopes describing the latlon area that should be used to constrain
* all queries
*
* @return
*/
private List<Envelope> getLatLonEnvelopes() {
if (latLonEnvelopes == null) {
this.latLonEnvelopes = new ArrayList<Envelope>(2);
try {
Geometry intersection = EnvelopeIntersection
.createEnvelopeIntersection(descriptorEnvelope,
WORLD_LAT_LON_ENVELOPE, 0.1, 180, 180);
if (intersection instanceof GeometryCollection) {
GeometryCollection gc = (GeometryCollection) intersection;
for (int n = 0; n < gc.getNumGeometries(); n += 1) {
latLonEnvelopes.add(gc.getGeometryN(n)
.getEnvelopeInternal());
}
} else {
latLonEnvelopes.add(intersection.getEnvelopeInternal());
}
} catch (Exception e) {
statusHandler.handle(Priority.VERBOSE, e.getLocalizedMessage(),
e);
this.latLonEnvelopes.add(WORLD_LAT_LON_ENVELOPE);
}
}
return latLonEnvelopes;
}
/**
* Subtract the precip amounts in map2 from those in map1 for all stations
* that are in both maps.
*
* @param map1
* @param map2
* @return
*/
private Map<String, PrecipData> subtract(Map<String, PrecipData> map1,
Map<String, PrecipData> map2) {
Map<String, PrecipData> result = new HashMap<String, PrecipData>();
@ -364,6 +532,14 @@ public class MetarPrecipDataContainer {
return result;
}
/**
* Subtract the precip amounts in several maps for all stations that are in
* all maps.
*
* @param map1
* @param map2
* @return
*/
private Map<String, PrecipData> add(Map<String, PrecipData>... maps) {
Map<String, PrecipData> result = new HashMap<String, PrecipData>();
for (Map<String, PrecipData> map : maps) {
@ -387,6 +563,13 @@ public class MetarPrecipDataContainer {
return result;
}
/**
* combine all maps so there is only one entry for each station. For
* stations in multiple maps the entry from the first map is used.
*
* @param maps
* @return
*/
private Map<String, PrecipData> combine(Map<String, PrecipData>... maps) {
Map<String, PrecipData> result = new HashMap<String, PrecipData>();
List<Map<String, PrecipData>> mapsList = new ArrayList<Map<String, PrecipData>>(