/// <summary> /// Summarise the features of an input feature class that fall within an input polygon. /// Input features can be points, lines, or polygons. A value field and a category field can be provided. /// If a category field is provided then this will be used like a GROUP BY clause in SQL and return will be broken down by /// category, in addition to the overall totals. /// If a value field is provided then it will be totalled (by category if provided), e.g. population in a polygon. /// But as some line / polygon features may be only partially contained we need to decide how to handle the values on those. /// Currently we just scale based on the proportion of the original feature that is included but there would be /// alternatives: count all or nothing based on inclusion of majority of feature, or centre of feature. Not yet implemented. /// </summary> /// <param name="pInputPolygon"> /// The IPolygon which will be used to clip and summarise the features from the second parameter /// </param> /// <param name="pInputFeatures"> /// The features which will be clipped and summarised. IFeatureClass that must be of ShapeType /// esriGeometryPoint, esriGeometryPolyline or esriGeometryPolygon /// </param> /// <param name="pCategoryFieldNum"> /// ID (integer) of a field in the feature class containing values by which the results should be grouped. Can be /// any field type but integer, string, etc are recommended. Value of -1 means no summation by category will occur. /// Set to -1 to not do this summary /// </param> /// <param name="pValueFieldNum"> /// ID (integer of a field in the feature class containing values by which the results should be totalled. For example /// population of counties, value of land parcels. Done in addition to totalling area / length / count. /// Set to -1 to not do this summary /// </param> /// <param name="pMeasureFieldNum"> /// ID (integer) of a field in the feature class containing pre-calculated values for area (polygons) or length (lines). /// Can be used to speed calculation of these properties. They will be calculated manually for features partially /// within the input polygon. /// </param> /// <returns> /// FeatureExtractionResult object containing: /// total feature count, /// total feature length / area (for lines / polygons), /// total feature value (from value field if provided), /// plus each of the above broken down by category if a category field is provided /// category results are each a dictionary of key=category value (as string), value=int or double /// </returns> internal static FeatureExtractionResult SummariseFeatures(IPolygon pInputPolygon, ExtractionLayerConfig pExtractionLayerConfig) //IFeatureClass pInputFeatures,int pCategoryFieldNum, int pValueFieldNum, int pMeasureFieldNum) { // use cast rather than as to make sure it blows up if the geodataset isn't // a feature class IFeatureClass pInputFeatures = (IFeatureClass)pExtractionLayerConfig.LayerDataset; esriGeometryType tFCType = pInputFeatures.ShapeType; // set up variables to build results Dictionary <string, int> tCategoryCounts = new Dictionary <string, int>(); Dictionary <string, double> tCategoryTotals = new Dictionary <string, double>(), tCategoryMeasures = new Dictionary <string, double>(); double tTotalMeasure = 0, tTotalValue = 0; int tTotalCount = 0; // variables to control search bool hasCategories = pExtractionLayerConfig.HasCategories; bool hasMeasures = pExtractionLayerConfig.HasMeasures; bool hasPreCalcMeasures = hasMeasures && (pExtractionLayerConfig.MeasureField != -1); bool hasValues = false; if (pExtractionLayerConfig.HasValues && pExtractionLayerConfig.ValueField != -1) { // only numeric fields will be totalled //esriFieldType tValueFieldType = pInputFeatures.Fields.get_Field(pValueFieldNum).Type; esriFieldType tValueFieldType = pInputFeatures.Fields.get_Field(pExtractionLayerConfig.ValueField) .Type; if (tValueFieldType == esriFieldType.esriFieldTypeDouble || tValueFieldType == esriFieldType.esriFieldTypeInteger || tValueFieldType == esriFieldType.esriFieldTypeSingle || tValueFieldType == esriFieldType.esriFieldTypeSmallInteger) { hasValues = true; } } // use a spatial filter to do the geographic selection ISpatialFilter tSpatialFilter = new SpatialFilterClass(); tSpatialFilter.Geometry = pInputPolygon as IGeometry; // first we will select all features wholly within the polygon. We don't need to do anything special with these // just use them as is. This applies for points, lines and polys. It is the only thing required for points. tSpatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelContains; tSpatialFilter.GeometryField = "SHAPE"; // safe to use a recycling cursor: we are not maintaining a reference to features across multiple calls to NextFeature IFeatureCursor tFeatureCursor = pInputFeatures.Search(tSpatialFilter, true); IFeature tThisFeature = tFeatureCursor.NextFeature(); try { while (tThisFeature != null) { tTotalCount += 1; double tMeasure = 0; if (hasPreCalcMeasures) { try { //tMeasure = (double)tThisFeature.get_Value(pMeasureFieldNum); tMeasure = (double)tThisFeature.get_Value(pExtractionLayerConfig.MeasureField); } catch { hasPreCalcMeasures = false; if (tFCType == esriGeometryType.esriGeometryPolyline) { IPolyline tFeatureAsLine = tThisFeature.Shape as IPolyline; tMeasure = tFeatureAsLine.Length; } else if (tFCType == esriGeometryType.esriGeometryPolygon) { IArea tFeatureAsArea = tThisFeature.Shape as IArea; tMeasure = tFeatureAsArea.Area; } } } else { if (tFCType == esriGeometryType.esriGeometryPolyline) { IPolyline tFeatureAsLine = tThisFeature.Shape as IPolyline; tMeasure = tFeatureAsLine.Length; } else if (tFCType == esriGeometryType.esriGeometryPolygon) { IArea tFeatureAsArea = tThisFeature.Shape as IArea; tMeasure = tFeatureAsArea.Area; } } tTotalMeasure += tMeasure; if (hasCategories) { // get the category / class of this featue string tCategory = tThisFeature.get_Value(pExtractionLayerConfig.CategoryField).ToString(); // placeholders for the dictionary lookups (out variables) int tCurrentCategoryCount; double tCurrentCategoryMeasure; // add 1 to the appropriate category count in the category counts dictionary if (tCategoryCounts.TryGetValue(tCategory, out tCurrentCategoryCount)) { tCategoryCounts[tCategory] = tCurrentCategoryCount + 1; } else { tCategoryCounts[tCategory] = 1; } if (tCategoryMeasures.TryGetValue(tCategory, out tCurrentCategoryMeasure)) { tCategoryMeasures[tCategory] = tCurrentCategoryMeasure + tMeasure; } else { tCategoryMeasures[tCategory] = tMeasure; } if (hasValues) { // i.e. look up the value from another field, other than just the feature's length/area and count double tCurrentCategoryTotal; //double tCurrentArcValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentArcValue = (double)tThisFeature.get_Value(pExtractionLayerConfig.ValueField); tTotalValue += tCurrentArcValue; if (tCategoryTotals.TryGetValue(tCategory, out tCurrentCategoryTotal)) { tCategoryTotals[tCategory] = tCurrentCategoryTotal + tCurrentArcValue; } else { tCategoryTotals[tCategory] = tCurrentArcValue; } } } else if (hasValues) { //double tCurrentArcValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentArcValue = (double)tThisFeature.get_Value(pExtractionLayerConfig.ValueField); tTotalValue += tCurrentArcValue; } tThisFeature = tFeatureCursor.NextFeature(); } // now for lines and polygons we need to process the features that are partially inside the polygon. // this process would work on all the features but there is no point doing expensive intersections where we // don't need to, so we did the wholly-contained features separately // we need to find the features where there is an intersection between the polygon boundary and the feature's // interior. Could use shape description language but this is covered by available spatialrelenum values bool doPartialFeatures = ((tFCType == esriGeometryType.esriGeometryPolyline || tFCType == esriGeometryType.esriGeometryPolygon)); // a point is either in or out! // not yet implemented: control how partially-intersecting features are handled //&& pFeatureIntersectionMode != IntersectingFeatureSelectionMode.SelectNoPartialFeatures); if (doPartialFeatures) { if (tFCType == esriGeometryType.esriGeometryPolyline) { // "A polyline and a polygon cross if they share a polyline or a point (for vertical line) in common on the // interior of the polygon which is not equivalent to the entire polyline." tSpatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelCrosses; } else if (tFCType == esriGeometryType.esriGeometryPolygon) { // "Two geometries overlap if the region of their intersection is of the same dimension as the geometries involved // and is not equivalent to either of the geometries." tSpatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelOverlaps; } // no longer safe to use recycling cursor as we're going to tinker with the features returned tFeatureCursor = pInputFeatures.Search(tSpatialFilter, false); tThisFeature = tFeatureCursor.NextFeature(); ITopologicalOperator tInputAsTopoOp = pInputPolygon as ITopologicalOperator; int tNumberCrossingBoundary = 0; while (tThisFeature != null) { // return (so track) the number of partially-included features separately from the overall total tNumberCrossingBoundary += 1; tTotalCount += 1; // either the length or area of the intersected feature: double tMeasure = 0; // either the length or area of entire original intersected feature // (to get proportion that's included): double tOriginalMeasure = 0; // do the IGeometry tFeatureGeometry = tThisFeature.ShapeCopy; if (tFCType == esriGeometryType.esriGeometryPolyline) { IPolyline tArcEntire = tFeatureGeometry as IPolyline; tOriginalMeasure = tArcEntire.Length; IPolyline tArcInside = tInputAsTopoOp.Intersect(tFeatureGeometry, esriGeometryDimension.esriGeometry1Dimension) as IPolyline; tMeasure = tArcInside.Length; } else { IArea tAreaEntire = tFeatureGeometry as IArea; tOriginalMeasure = tAreaEntire.Area; IArea tAreaInside = tInputAsTopoOp.Intersect(tFeatureGeometry, esriGeometryDimension.esriGeometry2Dimension) as IArea; tMeasure = tAreaInside.Area; } tTotalMeasure += tMeasure; if (hasCategories) { //string tCategory = tThisFeature.get_Value(pCategoryFieldNum).ToString(); string tCategory = tThisFeature.get_Value(pExtractionLayerConfig.CategoryField).ToString(); int tCurrentCategoryCount; double tCurrentCategoryMeasure; if (tCategoryCounts.TryGetValue(tCategory, out tCurrentCategoryCount)) { tCategoryCounts[tCategory] = tCurrentCategoryCount + 1; } else { tCategoryCounts[tCategory] = 1; } if (tCategoryMeasures.TryGetValue(tCategory, out tCurrentCategoryMeasure)) { tCategoryMeasures[tCategory] = tCurrentCategoryMeasure + tMeasure; } else { tCategoryMeasures[tCategory] = tMeasure; } if (hasValues) { // how should we handle a value field in an intersected feature? we can't, for certain, // as we don't know what they mean. We'll just assume that it scales proportionally with the // proportion of the original feature's length / area that is included. // The raster equivalent is to count all or none based on cell centre, so maybe we should count // all or none based on centroid?? double tCurrentCategoryTotal; //double tCurrentFeatureValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentFeatureValue = (double)tThisFeature.get_Value( pExtractionLayerConfig.ValueField); double tScaledFeatureValue = (tMeasure / tOriginalMeasure) * tCurrentFeatureValue; tTotalValue += tScaledFeatureValue; if (tCategoryTotals.TryGetValue(tCategory, out tCurrentCategoryTotal)) { tCategoryTotals[tCategory] = tCurrentCategoryTotal + tScaledFeatureValue; } else { tCategoryTotals[tCategory] = tScaledFeatureValue; } } } else if (hasValues) { //double tCurrentFeatureValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentFeatureValue = (double)tThisFeature.get_Value(pExtractionLayerConfig.ValueField); double tScaledFeatureValue = (tMeasure / tOriginalMeasure) * tCurrentFeatureValue; tTotalValue += tScaledFeatureValue; } tThisFeature = tFeatureCursor.NextFeature(); } } double?outMeasure; double?outValue; if (hasMeasures) { outMeasure = tTotalMeasure; } else { outMeasure = null; } if (hasValues) { outValue = tTotalValue; } else { outValue = null; } FeatureExtractionResult tResult = new FeatureExtractionResult( pExtractionLayerConfig.ParamName, tTotalCount, outMeasure, outValue, tCategoryCounts, tCategoryMeasures, tCategoryTotals, pInputFeatures.ShapeType ); return(tResult); } catch (Exception ex) { logger.LogMessage(ServerLogger.msgType.debug, "process features", 99, "error summarising features in " + pInputFeatures.AliasName + " Detail: " + ex.StackTrace + " " + ex.Message); return(new FeatureExtractionResult("An error occurred with extraction from " + pInputFeatures.AliasName, pExtractionLayerConfig.ParamName)); } }
//IFeatureClass pInputFeatures,int pCategoryFieldNum, int pValueFieldNum, int pMeasureFieldNum) /// <summary> /// Summarise the features of an input feature class that fall within an input polygon. /// Input features can be points, lines, or polygons. A value field and a category field can be provided. /// If a category field is provided then this will be used like a GROUP BY clause in SQL and return will be broken down by /// category, in addition to the overall totals. /// If a value field is provided then it will be totalled (by category if provided), e.g. population in a polygon. /// But as some line / polygon features may be only partially contained we need to decide how to handle the values on those. /// Currently we just scale based on the proportion of the original feature that is included but there would be /// alternatives: count all or nothing based on inclusion of majority of feature, or centre of feature. Not yet implemented. /// </summary> /// <param name="pInputPolygon"> /// The IPolygon which will be used to clip and summarise the features from the second parameter /// </param> /// <param name="pInputFeatures"> /// The features which will be clipped and summarised. IFeatureClass that must be of ShapeType /// esriGeometryPoint, esriGeometryPolyline or esriGeometryPolygon /// </param> /// <param name="pCategoryFieldNum"> /// ID (integer) of a field in the feature class containing values by which the results should be grouped. Can be /// any field type but integer, string, etc are recommended. Value of -1 means no summation by category will occur. /// Set to -1 to not do this summary /// </param> /// <param name="pValueFieldNum"> /// ID (integer of a field in the feature class containing values by which the results should be totalled. For example /// population of counties, value of land parcels. Done in addition to totalling area / length / count. /// Set to -1 to not do this summary /// </param> /// <param name="pMeasureFieldNum"> /// ID (integer) of a field in the feature class containing pre-calculated values for area (polygons) or length (lines). /// Can be used to speed calculation of these properties. They will be calculated manually for features partially /// within the input polygon. /// </param> /// <returns> /// FeatureExtractionResult object containing: /// total feature count, /// total feature length / area (for lines / polygons), /// total feature value (from value field if provided), /// plus each of the above broken down by category if a category field is provided /// category results are each a dictionary of key=category value (as string), value=int or double /// </returns> internal static FeatureExtractionResult SummariseFeatures(IPolygon pInputPolygon, ExtractionLayerConfig pExtractionLayerConfig) { // use cast rather than as to make sure it blows up if the geodataset isn't // a feature class IFeatureClass pInputFeatures = (IFeatureClass)pExtractionLayerConfig.LayerDataset; esriGeometryType tFCType = pInputFeatures.ShapeType; // set up variables to build results Dictionary<string, int> tCategoryCounts = new Dictionary<string, int>(); Dictionary<string, double> tCategoryTotals = new Dictionary<string, double>(), tCategoryMeasures = new Dictionary<string, double>(); double tTotalMeasure = 0, tTotalValue = 0; int tTotalCount = 0; // variables to control search bool hasCategories = pExtractionLayerConfig.HasCategories; bool hasMeasures = pExtractionLayerConfig.HasMeasures; bool hasPreCalcMeasures = hasMeasures && (pExtractionLayerConfig.MeasureField != -1); bool hasValues = false; if (pExtractionLayerConfig.HasValues && pExtractionLayerConfig.ValueField != -1) { // only numeric fields will be totalled //esriFieldType tValueFieldType = pInputFeatures.Fields.get_Field(pValueFieldNum).Type; esriFieldType tValueFieldType = pInputFeatures.Fields.get_Field(pExtractionLayerConfig.ValueField) .Type; if (tValueFieldType == esriFieldType.esriFieldTypeDouble || tValueFieldType == esriFieldType.esriFieldTypeInteger || tValueFieldType == esriFieldType.esriFieldTypeSingle || tValueFieldType == esriFieldType.esriFieldTypeSmallInteger) { hasValues = true; } } // use a spatial filter to do the geographic selection ISpatialFilter tSpatialFilter = new SpatialFilterClass(); tSpatialFilter.Geometry = pInputPolygon as IGeometry; // first we will select all features wholly within the polygon. We don't need to do anything special with these // just use them as is. This applies for points, lines and polys. It is the only thing required for points. tSpatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelContains; tSpatialFilter.GeometryField = "SHAPE"; // safe to use a recycling cursor: we are not maintaining a reference to features across multiple calls to NextFeature IFeatureCursor tFeatureCursor = pInputFeatures.Search(tSpatialFilter, true); IFeature tThisFeature = tFeatureCursor.NextFeature(); try { while (tThisFeature != null) { tTotalCount += 1; double tMeasure = 0; if (hasPreCalcMeasures) { try { //tMeasure = (double)tThisFeature.get_Value(pMeasureFieldNum); tMeasure = (double)tThisFeature.get_Value(pExtractionLayerConfig.MeasureField); } catch { hasPreCalcMeasures = false; if (tFCType == esriGeometryType.esriGeometryPolyline) { IPolyline tFeatureAsLine = tThisFeature.Shape as IPolyline; tMeasure = tFeatureAsLine.Length; } else if (tFCType == esriGeometryType.esriGeometryPolygon) { IArea tFeatureAsArea = tThisFeature.Shape as IArea; tMeasure = tFeatureAsArea.Area; } } } else { if (tFCType == esriGeometryType.esriGeometryPolyline) { IPolyline tFeatureAsLine = tThisFeature.Shape as IPolyline; tMeasure = tFeatureAsLine.Length; } else if (tFCType == esriGeometryType.esriGeometryPolygon) { IArea tFeatureAsArea = tThisFeature.Shape as IArea; tMeasure = tFeatureAsArea.Area; } } tTotalMeasure += tMeasure; if (hasCategories) { // get the category / class of this featue string tCategory = tThisFeature.get_Value(pExtractionLayerConfig.CategoryField).ToString(); // placeholders for the dictionary lookups (out variables) int tCurrentCategoryCount; double tCurrentCategoryMeasure; // add 1 to the appropriate category count in the category counts dictionary if (tCategoryCounts.TryGetValue(tCategory, out tCurrentCategoryCount)) { tCategoryCounts[tCategory] = tCurrentCategoryCount + 1; } else { tCategoryCounts[tCategory] = 1; } if (tCategoryMeasures.TryGetValue(tCategory, out tCurrentCategoryMeasure)) { tCategoryMeasures[tCategory] = tCurrentCategoryMeasure + tMeasure; } else { tCategoryMeasures[tCategory] = tMeasure; } if (hasValues) { // i.e. look up the value from another field, other than just the feature's length/area and count double tCurrentCategoryTotal; //double tCurrentArcValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentArcValue = (double)tThisFeature.get_Value(pExtractionLayerConfig.ValueField); tTotalValue += tCurrentArcValue; if (tCategoryTotals.TryGetValue(tCategory, out tCurrentCategoryTotal)) { tCategoryTotals[tCategory] = tCurrentCategoryTotal + tCurrentArcValue; } else { tCategoryTotals[tCategory] = tCurrentArcValue; } } } else if (hasValues) { //double tCurrentArcValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentArcValue = (double)tThisFeature.get_Value(pExtractionLayerConfig.ValueField); tTotalValue += tCurrentArcValue; } tThisFeature = tFeatureCursor.NextFeature(); } // now for lines and polygons we need to process the features that are partially inside the polygon. // this process would work on all the features but there is no point doing expensive intersections where we // don't need to, so we did the wholly-contained features separately // we need to find the features where there is an intersection between the polygon boundary and the feature's // interior. Could use shape description language but this is covered by available spatialrelenum values bool doPartialFeatures = ((tFCType == esriGeometryType.esriGeometryPolyline || tFCType == esriGeometryType.esriGeometryPolygon) ); // a point is either in or out! // not yet implemented: control how partially-intersecting features are handled //&& pFeatureIntersectionMode != IntersectingFeatureSelectionMode.SelectNoPartialFeatures); if (doPartialFeatures){ if (tFCType == esriGeometryType.esriGeometryPolyline) { // "A polyline and a polygon cross if they share a polyline or a point (for vertical line) in common on the // interior of the polygon which is not equivalent to the entire polyline." tSpatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelCrosses; } else if (tFCType == esriGeometryType.esriGeometryPolygon) { // "Two geometries overlap if the region of their intersection is of the same dimension as the geometries involved // and is not equivalent to either of the geometries." tSpatialFilter.SpatialRel = esriSpatialRelEnum.esriSpatialRelOverlaps; } // no longer safe to use recycling cursor as we're going to tinker with the features returned tFeatureCursor = pInputFeatures.Search(tSpatialFilter, false); tThisFeature = tFeatureCursor.NextFeature(); ITopologicalOperator tInputAsTopoOp = pInputPolygon as ITopologicalOperator; int tNumberCrossingBoundary = 0; while (tThisFeature != null) { // return (so track) the number of partially-included features separately from the overall total tNumberCrossingBoundary += 1; tTotalCount += 1; // either the length or area of the intersected feature: double tMeasure = 0; // either the length or area of entire original intersected feature // (to get proportion that's included): double tOriginalMeasure = 0; // do the IGeometry tFeatureGeometry = tThisFeature.ShapeCopy; if (tFCType == esriGeometryType.esriGeometryPolyline) { IPolyline tArcEntire = tFeatureGeometry as IPolyline; tOriginalMeasure = tArcEntire.Length; IPolyline tArcInside = tInputAsTopoOp.Intersect(tFeatureGeometry, esriGeometryDimension.esriGeometry1Dimension) as IPolyline; tMeasure = tArcInside.Length; } else { IArea tAreaEntire = tFeatureGeometry as IArea; tOriginalMeasure = tAreaEntire.Area; IArea tAreaInside = tInputAsTopoOp.Intersect(tFeatureGeometry, esriGeometryDimension.esriGeometry2Dimension) as IArea; tMeasure = tAreaInside.Area; } tTotalMeasure += tMeasure; if (hasCategories) { //string tCategory = tThisFeature.get_Value(pCategoryFieldNum).ToString(); string tCategory = tThisFeature.get_Value(pExtractionLayerConfig.CategoryField).ToString(); int tCurrentCategoryCount; double tCurrentCategoryMeasure; if (tCategoryCounts.TryGetValue(tCategory, out tCurrentCategoryCount)) { tCategoryCounts[tCategory] = tCurrentCategoryCount + 1; } else { tCategoryCounts[tCategory] = 1; } if (tCategoryMeasures.TryGetValue(tCategory, out tCurrentCategoryMeasure)) { tCategoryMeasures[tCategory] = tCurrentCategoryMeasure + tMeasure; } else { tCategoryMeasures[tCategory] = tMeasure; } if (hasValues) { // how should we handle a value field in an intersected feature? we can't, for certain, // as we don't know what they mean. We'll just assume that it scales proportionally with the // proportion of the original feature's length / area that is included. // The raster equivalent is to count all or none based on cell centre, so maybe we should count // all or none based on centroid?? double tCurrentCategoryTotal; //double tCurrentFeatureValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentFeatureValue = (double)tThisFeature.get_Value( pExtractionLayerConfig.ValueField); double tScaledFeatureValue = (tMeasure / tOriginalMeasure) * tCurrentFeatureValue; tTotalValue += tScaledFeatureValue; if (tCategoryTotals.TryGetValue(tCategory,out tCurrentCategoryTotal)) { tCategoryTotals[tCategory] = tCurrentCategoryTotal + tScaledFeatureValue; } else { tCategoryTotals[tCategory] = tScaledFeatureValue; } } } else if (hasValues) { //double tCurrentFeatureValue = (double)tThisFeature.get_Value(pValueFieldNum); double tCurrentFeatureValue = (double)tThisFeature.get_Value(pExtractionLayerConfig.ValueField); double tScaledFeatureValue = (tMeasure / tOriginalMeasure)*tCurrentFeatureValue; tTotalValue += tScaledFeatureValue; } tThisFeature = tFeatureCursor.NextFeature(); } } double? outMeasure; double? outValue; if (hasMeasures) { outMeasure = tTotalMeasure; } else { outMeasure = null; } if (hasValues) { outValue = tTotalValue; } else { outValue = null; } FeatureExtractionResult tResult = new FeatureExtractionResult( pExtractionLayerConfig.ParamName, tTotalCount, outMeasure, outValue, tCategoryCounts, tCategoryMeasures, tCategoryTotals, pInputFeatures.ShapeType ); return tResult; } catch (Exception ex) { logger.LogMessage(ServerLogger.msgType.debug, "process features", 99, "error summarising features in " + pInputFeatures.AliasName+" Detail: " + ex.StackTrace + " " + ex.Message); return new FeatureExtractionResult("An error occurred with extraction from "+pInputFeatures.AliasName,pExtractionLayerConfig.ParamName); } }