protected override void InternalGET(System.Web.HttpContext context, HandlerTimedCache cache) { IList <SecurityRole> roles = UserHelper.GetUserRoles(context.User.Identity.Name); //Get the paging parameters... int page = WebUtil.ParseIntParam(context, "page"); int pageSize = WebUtil.ParseIntParam(context, "pageSize"); // Check to see if this is a csv export request. Runs the normal query (with no paging). bool csv = false; WebUtil.ParseOptionalBoolParam(context, "csv", ref csv); // If this is csv, we want all data - override any paging if (csv) { page = -1; pageSize = -1; } // Now get the ordering parameters, if specified. int sortCol = -1; WebUtil.ParseOptionalIntParam(context, "sortBy", ref sortCol); SortType?sortDir = null; if (sortCol >= 0) { // Default is ascending sort, passing false means descending. bool ascending = true; WebUtil.ParseOptionalBoolParam(context, "sortasc", ref ascending); sortDir = ascending ? SortType.Asc : SortType.Desc; } string indicatorId = WebUtil.GetParam(context, "indicator", false); NycResolutionType resolution = WebUtil.ParseEnumParam <NycResolutionType>(context, "resolution"); NycTimeframeType timetype = WebUtil.ParseEnumParam <NycTimeframeType>(context, "timetype"); int minyear = WebUtil.ParseIntParam(context, "minyear"); int maxyear = WebUtil.ParseIntParam(context, "maxyear"); // These two params are for "scope". These should be "ActualId" not "UID". string borough = WebUtil.GetParam(context, "borough", true); string subborough = WebUtil.GetParam(context, "subborough", true); NycResultsWithMetadata list = NychanisHelper.Query(indicatorId, resolution, timetype, minyear, maxyear, borough, subborough, sortCol, sortDir, pageSize, page); // If this was a csv request, format it and return it instead if (csv) { // Generate actual csv data, determine if this is groupby'd or not string export = NychanisHelper.ResultsAsCsv(list, indicatorId); // Setup the response to handle this type of request context.Response.AddHeader("Content-Disposition", "attachment;filename=Furman_Center_Neighborhood_Info.csv"); context.Response.ContentType = "text/csv"; context.Response.Write(export); return; } // Return the results to the client context.Response.Write(WebUtil.ObjectToJson(list)); }
/// <summary> /// Extract its info from a geography. /// </summary> /// <param name="geog"></param> public NycResolution(NycGeography geog) { Name = geog.ResolutionName; UID = geog.Resolution; Order = geog.ResolutionOrder; HasBoroughData = StringHelper.IsNonBlank(geog.Borough); HasSubBoroughData = StringHelper.IsNonBlank(geog.SubBorough); Geographies.Add(new ThinNycGeography(geog)); }
protected override void InternalGET(HttpContext context, HandlerTimedCache cache) { string indicatorId = WebUtil.GetParam(context, "indicator", false); NycResolutionType resolution = WebUtil.ParseEnumParam <NycResolutionType>(context, "resolution"); string timeId = WebUtil.GetParam(context, "time", false); // These two params are for "scope". These should be "ActualId" not "UID". string borough = WebUtil.GetParam(context, "borough", true); string subborough = WebUtil.GetParam(context, "subborough", true); string sld = NychanisHelper.GenerateSld(indicatorId, resolution, timeId, borough, subborough); context.Response.ContentType = "text/xml"; context.Response.Write(sld); }
/// <summary> /// Runs a query for the data for a particular indicator/resolution/time, /// and generates an SLD to color the geographies correctly based on the values. /// </summary> /// <param name="indicatorId"></param> /// <param name="resolutionType"></param> /// <param name="timeId"></param> /// <param name="scopeBorough"></param> /// <param name="scopeSubborough"></param> /// <returns></returns> public static string GenerateSld(object indicatorId, NycResolutionType resolutionType, object timeId, object scopeBorough, object scopeSubborough) { // First load the indicator metadata, both because we need the info, and because // if the query is for data that doesn't exist, we can skip a lot of work. NycIndicator indicator = GetIndicator(indicatorId); // Don't validate time, since there's no easy way to do it other than querying and seeing // if we get anything. // Now load the configs that tell us what color to render everything as. Config cfg = Config.GetConfig("NYC.SLD"); // Acceptable values are 0 to 100. So we need a 101 length array. string[] colors = new string[101]; string color = null; IDictionary <string, IList <object> > geogIdsByColor = new CheckedDictionary <string, IList <object> >(); for (int x = 0; x < colors.Length; x++) { // If the value isn't in the config, use the same as the last value. color = cfg.GetParameter(indicator.UseAlternateColors ? "AlternateBreakpoints" : "BreakPoints", x.ToString(), color); // Make sure we have a list created for the geogs that will be this color. if ((color != null) && (!geogIdsByColor.ContainsKey(color))) { geogIdsByColor[color] = new List <object>(); } colors[x] = color; } string mapLayerName = cfg.GetParameter("LayerNames", resolutionType.ToString(), null); string layerGeogIdField = cfg.GetParameter("LayerGeographyIdFields", resolutionType.ToString(), null); string layerDisplayNameField = cfg.GetParameter("LayerDisplayNameFields", resolutionType.ToString(), null); // This includes any labeling and outlines, default background color, etc. StringBuilder sldRules = new StringBuilder(cfg.GetConfigInnerXml("DefaultSLD")); // Replace any tokens with the appropriate values for this layer. sldRules.Replace("{LayerName}", mapLayerName); sldRules.Replace("{LayerGeographyIdField}", layerGeogIdField); sldRules.Replace("{LayerDisplayNameField}", layerDisplayNameField); // If no map layer is defined (I.E. "City"), or the request is invalid in some way, // use just the default SLD. if (mapLayerName != null) { // Now query for all the data. DaoCriteria crit = new DaoCriteria(); crit.Expressions.Add(new EqualExpression("IndicatorId", indicatorId)); crit.Expressions.Add(new EqualExpression("Resolution", resolutionType)); crit.Expressions.Add(new EqualExpression("TimeId", timeId)); DaoCriteria geogCrit = null; if (scopeBorough != null) { geogCrit = new DaoCriteria(); geogCrit.Expressions.Add(new EqualExpression("Borough", scopeBorough)); if (scopeSubborough != null) { geogCrit.Expressions.Add(new EqualExpression("SubBorough", scopeSubborough)); } } if (geogCrit != null) { // Scope and/or subscope was defined. geogCrit.Expressions.Add(new EqualExpression("Resolution", resolutionType)); IList <object> geogIdsInScope = new List <object>(); foreach (NycGeography geog in _geogDao.Get(geogCrit)) { geogIdsInScope.Add(geog.UID); } crit.Expressions.Add(new PropertyInListExpression("GeographyId", geogIdsInScope)); } IList <NycDatum> data = _dataDao.Get(crit); // If there's no data, don't make any SLD rules. if ((data != null) && (data.Count > 0)) { foreach (NycDatum datum in data) { int val; switch (indicator.Breakpoint) { case NycBreakpointType.HistoricalBreakpoint: val = datum.HistoricalBreakpoint; break; case NycBreakpointType.ContemporaryBreakpoint: val = datum.ContemporaryBreakpoint; break; default: throw new ArgumentOutOfRangeException("Indicator " + indicator + " has invalid breakpoint: " + indicator.Breakpoint); } if ((val < 0) || (val > 100)) { _log.Warn("'Normalized' value " + datum + " was outside 0-100 range."); } // Figure out, based on the value, which color group it belongs to. string thisColor = colors[val]; if (thisColor != null) { // Put it in that group. geogIdsByColor[thisColor].Add(datum.GeographyId); } } // Now append the rules for those color groups. foreach (KeyValuePair <string, IList <object> > kvp in geogIdsByColor) { // Only add rules for colors that actually have values. if (kvp.Value.Count > 0) { sldRules.Append("<Rule>"); // The part that says what geography IDs get this color. sldRules.Append("<ogc:Filter>"); sldRules.Append("<ogc:Or>"); foreach (object geogId in kvp.Value) { sldRules.Append("<ogc:PropertyIsEqualTo>"); sldRules.Append("<ogc:PropertyName>").Append(layerGeogIdField).Append( "</ogc:PropertyName>"); sldRules.Append("<ogc:Literal>").Append(geogId).Append("</ogc:Literal>"); sldRules.Append("</ogc:PropertyIsEqualTo>"); } sldRules.Append("</ogc:Or>"); sldRules.Append("</ogc:Filter>"); // The part that says what color to fill with. sldRules.Append("<PolygonSymbolizer>"); sldRules.Append("<Fill>"); sldRules.Append("<CssParameter name=\"fill\">").Append(kvp.Key).Append("</CssParameter>"); sldRules.Append("<CssParameter name=\"fill-opacity\">").Append( cfg.GetParameter("Polygons", "Opacity")).Append("</CssParameter>"); sldRules.Append("</Fill>"); sldRules.Append("</PolygonSymbolizer>"); sldRules.Append("</Rule>"); } } } } // Now wrap all the rendering rules in the SLD outer tags. StringBuilder sld = new StringBuilder(); sld.Append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n") .Append("<StyledLayerDescriptor version=\"1.0.0\"") .Append(" xsi:schemaLocation=\"http://www.opengis.net/sld StyledLayerDescriptor.xsd\"") .Append(" xmlns=\"http://www.opengis.net/sld\"") .Append(" xmlns:ogc=\"http://www.opengis.net/ogc\"") .Append(" xmlns:xlink=\"http://www.w3.org/1999/xlink\"") .Append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">") .Append("<NamedLayer>") .Append("<Name>").Append(mapLayerName).Append("</Name>") .Append("<UserStyle>") .Append("<FeatureTypeStyle>") .Append(sldRules.ToString()) .Append("</FeatureTypeStyle>") .Append("</UserStyle>") .Append("</NamedLayer>") .Append("</StyledLayerDescriptor>"); return(sld.ToString()); }
/// <summary> /// Generates a list of label/color pairs with which one can create a legend /// </summary> /// <returns></returns> public static NycLegendInfo GenerateLegendList(NycIndicator indicator, NycResolutionType resolution) { NycLegendInfo retVal = new NycLegendInfo(); // Get the color and breakpoint information from the config file Config cfg = Config.GetConfig("NYC.SLD"); retVal.Opacity = cfg.GetParameter("Polygons", "Opacity"); retVal.ValueType = (indicator.ValueType == null) ? null : indicator.ValueType.ToString(); IList <KeyValuePair <string, string> > paramList = cfg.GetParametersAsList(indicator.UseAlternateColors ? "AlternateBreakpoints" : "BreakPoints"); retVal.Elements = new List <NycLegendElement>(); // Loop through each named breakpoint and get a list of associated colors float absoluteMinVal; float absoluteMaxVal; switch (indicator.Breakpoint) { case NycBreakpointType.HistoricalBreakpoint: switch (resolution) { case NycResolutionType.Borough: absoluteMinVal = indicator.HistoricBoroughMin; absoluteMaxVal = indicator.HistoricBoroughMax; break; case NycResolutionType.CensusTract: absoluteMinVal = indicator.HistoricCensusTractMin; absoluteMaxVal = indicator.HistoricCensusTractMax; break; case NycResolutionType.City: absoluteMinVal = indicator.HistoricCityMin; absoluteMaxVal = indicator.HistoricCityMax; break; case NycResolutionType.CommunityDistrict: absoluteMinVal = indicator.HistoricCommunityDistrictMin; absoluteMaxVal = indicator.HistoricCommunityDistrictMax; break; case NycResolutionType.PolicePrecinct: absoluteMinVal = indicator.HistoricPolicePrecinctMin; absoluteMaxVal = indicator.HistoricPolicePrecinctMax; break; case NycResolutionType.SchoolDistrict: absoluteMinVal = indicator.HistoricSchoolDistrictMin; absoluteMaxVal = indicator.HistoricSchoolDistrictMax; break; case NycResolutionType.SubBorough: absoluteMinVal = indicator.HistoricSubBoroughMin; absoluteMaxVal = indicator.HistoricSubBoroughMax; break; default: throw new NotSupportedException("Resolution type " + resolution + " isn't supported yet."); } break; case NycBreakpointType.ContemporaryBreakpoint: switch (resolution) { case NycResolutionType.Borough: absoluteMinVal = indicator.ContemporaryBoroughMin; absoluteMaxVal = indicator.ContemporaryBoroughMax; break; case NycResolutionType.CensusTract: absoluteMinVal = indicator.ContemporaryCensusTractMin; absoluteMaxVal = indicator.ContemporaryCensusTractMax; break; case NycResolutionType.City: absoluteMinVal = indicator.ContemporaryCityMin; absoluteMaxVal = indicator.ContemporaryCityMax; break; case NycResolutionType.CommunityDistrict: absoluteMinVal = indicator.ContemporaryCommunityDistrictMin; absoluteMaxVal = indicator.ContemporaryCommunityDistrictMax; break; case NycResolutionType.PolicePrecinct: absoluteMinVal = indicator.ContemporaryPolicePrecinctMin; absoluteMaxVal = indicator.ContemporaryPolicePrecinctMax; break; case NycResolutionType.SchoolDistrict: absoluteMinVal = indicator.ContemporarySchoolDistrictMin; absoluteMaxVal = indicator.ContemporarySchoolDistrictMax; break; case NycResolutionType.SubBorough: absoluteMinVal = indicator.ContemporarySubBoroughMin; absoluteMaxVal = indicator.ContemporarySubBoroughMax; break; default: throw new NotSupportedException("Resolution type " + resolution + " isn't supported yet."); } break; default: throw new NotSupportedException("Breakpoint type " + indicator.Breakpoint + " isn't supported yet."); } float totalRange = absoluteMaxVal - absoluteMinVal; for (int i = 0; i < paramList.Count; i++) { NycLegendElement element = new NycLegendElement(); element.Color = paramList[i].Value; // Always start at the absolute min val. element.MinValue = absoluteMinVal; // If this is after the first one, add the fraction of the range. if (i > 0) { element.MinValue += ((float.Parse(paramList[i].Key) / 100.0f) * totalRange); } if (i + 1 < paramList.Count) { // This isn't the last element, so use the next one to get the range. element.MaxValue = ((float.Parse(paramList[i + 1].Key) / 100.0f) * totalRange) + absoluteMinVal; } else { // Last element, so always use the absolute max value. element.MaxValue = absoluteMaxVal; } // Add our range and color retVal.Elements.Add(element); } return(retVal); }
/// <summary> /// Queries for Nychanis data for the specified year range, resolution, and indicator. /// </summary> /// <param name="indicatorId">ID of the indicator being queried.</param> /// <param name="resolutionType">What resolution are we querying for.</param> /// <param name="timeUnitType">What type of time units should the data be in?</param> /// <param name="startYear">First year to include in the results.</param> /// <param name="endYear">Last year to include in the results.</param> public static NycResultsWithMetadata Query(object indicatorId, NycResolutionType resolutionType, NycTimeframeType timeUnitType, int startYear, int endYear) { // Overload with default ordering return(Query(indicatorId, resolutionType, timeUnitType, startYear, endYear, null, null, 0, SortType.Asc, -1, -1)); }
private static NycTableResults QueryNychanisData(object indicatorId, NycResolutionType resolutionType, object scopeBorough, object scopeSubborough, IDictionary <object, int> colsByTimeId) { // Query both the geography and data tables in a join so we can get the // nice display name of the geography rather than just the ID. NOTE: This could // be done by obtaining the geographies in a single query then the data in a second // query. At the moment this seems better. DaoJoinCriteria joinCrit = new DaoJoinCriteria(); joinCrit.Expressions.Add(new EqualJoinExpression("GeographyId", "UID")); // Add filters to the data (left) side of the join. DaoCriteria dataCrit = new DaoCriteria(); joinCrit.LeftCriteria = dataCrit; // Only the indicator that was requested. dataCrit.Expressions.Add(new EqualExpression("IndicatorId", indicatorId)); // Only the resolution that was requested. dataCrit.Expressions.Add(new EqualExpression("Resolution", resolutionType)); // Only the time span that was requested. dataCrit.Expressions.Add(new PropertyInListExpression("TimeId", colsByTimeId.Keys)); // Add support for querying for a single borough or subborough, "scope" DaoCriteria scopeCrit = new DaoCriteria(); if (scopeBorough != null) { scopeCrit.Expressions.Add(new EqualExpression("Borough", scopeBorough)); if (scopeSubborough != null) { scopeCrit.Expressions.Add(new EqualExpression("SubBorough", scopeSubborough)); } } // If we added any expressions, add the criteria to the geography side of the join if (scopeCrit.Expressions.Count > 0) { joinCrit.RightCriteria = scopeCrit; } // Sort the results by geography then by time (descending). This allows us to populate // the results structure easily, even if we later resort based on the user's choice. // We sort first by geography name, which "should" be unique, but just in case we also // sort by geography ID. joinCrit.Orders.Add(new JoinSortOrder("Name", false)); joinCrit.Orders.Add(new JoinSortOrder("GeographyId", true)); joinCrit.Orders.Add(new JoinSortOrder("TimeId", SortType.Desc, true)); IList <JoinResult <NycDatum, NycGeography> > dataAndGeogs = _dataDao.Get(joinCrit, _geogDao); NycTableResults results = new NycTableResults(); List <IList <object> > values = new List <IList <object> >(); // Now, for each geography ID, add all its year values to a list and add to the values collection. if (dataAndGeogs.Count > 0) { object lastGeogId = null; IList <object> resultArray = null; IList <bool> dataAvailableByTime = new bool[colsByTimeId.Count]; foreach (JoinResult <NycDatum, NycGeography> result in dataAndGeogs) { if ((lastGeogId == null) || (!lastGeogId.Equals(result.Left.GeographyId))) { // Create a new array, at the correct length already in case there // are holes in the data. resultArray = new object[colsByTimeId.Count + 1]; resultArray[0] = result.Right.Name; lastGeogId = result.Left.GeographyId; // Add it to the list to be returned. values.Add(resultArray); } // Always populate this value in the array. resultArray[colsByTimeId[result.Left.TimeId]] = result.Left.Value; dataAvailableByTime[colsByTimeId[result.Left.TimeId] - 1] = true; } results.DataAvailableByTime = dataAvailableByTime; } results.Values = values; return(results); }
/// <summary> /// Queries for Nychanis data for the specified year range, resolution, and indicator. /// </summary> /// <param name="indicatorId">ID of the indicator being queried.</param> /// <param name="resolutionType">What resolution are we querying for.</param> /// <param name="timeUnitType">What type of time units should the data be in?</param> /// <param name="startYear">First year to include in the results.</param> /// <param name="endYear">Last year to include in the results.</param> /// <param name="scopeSubborough"></param> /// <param name="scopeBorough"></param> /// <param name="orderCol">The column to sort by.</param> /// <param name="orderDir">The direction to sort, ignored if order is less than zero. /// <param name="numPerPage">Number of records to be returned in the page sequence, if not less than zero</param> /// <param name="page">Which page in this page sequence is this request for. /// If null, assumed to be ascending.</param> /// <returns>The results!</returns> public static NycResultsWithMetadata Query(object indicatorId, NycResolutionType resolutionType, NycTimeframeType timeUnitType, int startYear, int endYear, object scopeBorough, object scopeSubborough, int orderCol, SortType?orderDir, int numPerPage, int page) { NycResultsWithMetadata retVal = new NycResultsWithMetadata(); // First load the indicator metadata, both because we need the display name, and because // if the query is for data that doesn't exist, we can skip a lot of work. NycIndicator indicator = GetIndicator(indicatorId); retVal.Indicator = indicator.Name; retVal.Resolution = resolutionType.ToString(); // Now verify the query against the metadata. retVal.MinYear = (startYear < indicator.MinYear) ? indicator.MinYear : startYear; retVal.MaxYear = (endYear > indicator.MaxYear) ? indicator.MaxYear : endYear; bool validRequest = (retVal.MaxYear >= retVal.MinYear); if (!validRequest) { // Return a completely blank result object. return(retVal); } // We only want to load time metadata for times this indicator actually has data for. DaoJoinCriteria joinCrit = new DaoJoinCriteria(); joinCrit.RightCriteria = new DaoCriteria(); joinCrit.RightCriteria.Expressions.Add(new EqualExpression("IndicatorId", indicatorId)); joinCrit.RightCriteria.Expressions.Add(new EqualExpression("Resolution", resolutionType)); joinCrit.Expressions.Add(new EqualJoinExpression("UID", "TimeId")); // Load the time metadata. joinCrit.LeftCriteria = new DaoCriteria(); // These are not-ed so they are <= and >=; joinCrit.LeftCriteria.Expressions.Add(new GreaterExpression("Year", retVal.MaxYear, false)); joinCrit.LeftCriteria.Expressions.Add(new LesserExpression("Year", retVal.MinYear, false)); joinCrit.LeftCriteria.Expressions.Add(new EqualExpression("Type", timeUnitType)); // Sort by value descending, so the most recent year comes first. joinCrit.Orders.Add(new JoinSortOrder("Value", SortType.Asc, true)); List <JoinResult <NycTimeframe, NycResolutionForIndicator> > timeframes = _timeDao.Get(joinCrit, _resolutionDao); // We also need to know what all possible times are though, so we can render the fancy slider with appropriate gaps. joinCrit.LeftCriteria.Orders.Add(new SortOrder("Value", SortType.Asc)); IList <NycTimeframe> allTimeframes = _timeDao.Get(joinCrit.LeftCriteria); // Use them to assemble the metadata, since one year is one column. retVal.Attrs = new List <AbstractNamedSortable>(); retVal.Attrs.Add(new NycGeogColumnMetadata("Area")); IDictionary <object, int> colsByTimeId = new CheckedDictionary <object, int>(); foreach (JoinResult <NycTimeframe, NycResolutionForIndicator> timeframe in timeframes) { colsByTimeId[timeframe.Left.UID] = retVal.Attrs.Count; retVal.Attrs.Add(new NycYearColumnMetadata(timeframe.Left, indicator.ValueType)); } NycTableResults results = QueryNychanisData(indicatorId, resolutionType, scopeBorough, scopeSubborough, colsByTimeId); retVal.TotalResults = results.Values.Count; // Don't do any further processing if there are no results if (results.Values.Count == 0) { return(retVal); } // If the user specified a sort order, use that, otherwise sort by the first column (area). List <KeyValuePair <int, SortType> > colSorts = new List <KeyValuePair <int, SortType> >(); colSorts.Add(new KeyValuePair <int, SortType>(orderCol < 0 ? 0 : orderCol, orderDir ?? SortType.Asc)); results.Values.Sort(new MultipleColumnListComparer(colSorts)); // Truncate the results by the requested paging information retVal.Values = ResultsWithMetadata <AbstractNamedSortable> .GetPagedSubset(results.Values, numPerPage, page); // Now get context rows. Always get City, unless we're querying for City. if (resolutionType != NycResolutionType.City) { List <IList <object> > contextRows = QueryNychanisData(indicatorId, NycResolutionType.City, null, null, colsByTimeId).Values; // Now, if they provided a borough scope and didn't ask for a borough resolution, // include borough. if ((resolutionType != NycResolutionType.Borough) && (scopeBorough != null)) { contextRows.AddRange(QueryNychanisData(indicatorId, NycResolutionType.Borough, scopeBorough, null, colsByTimeId).Values); // Then if not subborough but a subborough is provided, include that. if ((resolutionType != NycResolutionType.SubBorough) && (scopeSubborough != null)) { contextRows.AddRange(QueryNychanisData(indicatorId, NycResolutionType.SubBorough, scopeBorough, scopeSubborough, colsByTimeId).Values); } } retVal.ContextRows = contextRows; } // Now generate the map info for showing the results as a cloropleth layer. Config cfg = Config.GetConfig("PDP.Data"); string sldHandlerUrl = cfg.GetParameter("Mapping", "SldHandlerURL"); retVal.MapInfo = new NycMapInfo(); retVal.MapInfo.Server = cfg.GetParameter("Mapping", "MapServerURL"); retVal.MapInfo.Layers = new List <NycLayerInfo>(); string layerName = Config.GetConfig("NYC.SLD").GetParameter("LayerNames", resolutionType.ToString(), null); // City doesn't have a map layer, so don't create any OL layers for it. if (layerName != null) { int possibleTimeIndex = 0; int actualTimeIndex = 0; while (possibleTimeIndex < allTimeframes.Count) { // We need to pad out the list of layers with blanks for timeframes that lack data. NycLayerInfo layer = new NycLayerInfo(); layer.Name = allTimeframes[possibleTimeIndex].Name; // Years that actually have data go up faster than all years, so check if the current // possible year is lower than the next actual year. if (allTimeframes[possibleTimeIndex].Value < timeframes[actualTimeIndex].Left.Value) { // Need to pad with a blank, so just let the blank layer object be added. // Increment the possible time index to the next timeframe. possibleTimeIndex++; } else { NycTimeframe time = timeframes[actualTimeIndex].Left; // I think this check is no longer necessary, since we're probably // always excluding unavailable data. But if it ain't broke... if (results.DataAvailableByTime[actualTimeIndex]) { layer.Config = new Dictionary <string, object>(); layer.Config["layers"] = layerName; layer.Config["styles"] = ""; layer.Config["format"] = "image/png"; layer.Config["tiled"] = true; layer.Config["srs"] = "EPSG:4326"; layer.Config["transparent"] = true; StringBuilder sb = new StringBuilder(sldHandlerUrl); sb.Append("?indicator=").Append(indicatorId); sb.Append("&resolution=").Append(resolutionType.ToString()); sb.Append("&time=").Append(time.UID); if (scopeBorough != null) { sb.Append("&borough=").Append(scopeBorough); if (scopeSubborough != null) { sb.Append("&subborough=").Append(scopeSubborough); } } layer.Config["SLD"] = sb.ToString(); } // Increment both indexes. possibleTimeIndex++; actualTimeIndex++; } retVal.MapInfo.Layers.Add(layer); } // If we are creating layers, we must create a legend to describe them retVal.LegendInfo = GenerateLegendList(indicator, resolutionType); } return(retVal); }