internal static RasterExtractionResult SummariseRaster(IGeoDataset pClippingPolygon, ExtractionLayerConfig pExtractionLayerConfig)
        {
            // set the analysis extent to be that of the polygon
            ServerLogger logger = new ServerLogger();
            logger.LogMessage(ServerLogger.msgType.debug, "SummariseCategoricalRaster", 99, "Categorical raster clip beginning..");
            IEnvelope tAnalysisExtent = pClippingPolygon.Extent;

            bool pCategoricalSummary = pExtractionLayerConfig.ExtractionType==ExtractionTypes.CategoricalRaster;
            IGeoDataset pRasterToClip = pExtractionLayerConfig.LayerDataset;

            IRasterAnalysisEnvironment tRasterAnalysisEnvironment = new RasterAnalysisClass();
            object tAnalysisEnvelopeCastToObject = (System.Object)tAnalysisExtent;
               // object tAnotherBizarreMissingObject = Type.Missing;
            object tSnapObject = (System.Object)pRasterToClip;
            tRasterAnalysisEnvironment.SetExtent(esriRasterEnvSettingEnum.esriRasterEnvValue,
                ref tAnalysisEnvelopeCastToObject, ref tSnapObject);
            tRasterAnalysisEnvironment.SetAsNewDefaultEnvironment();
            // extract the subset of the raster
            IExtractionOp2 tExtractionOp = new RasterExtractionOpClass();
            // note we want to base the extraction on a raster (in an IGeoDataset) rather than an IPolygon.
            // That's because the catchment polygon may be multipart, and the operation doesn't work with multipart.
            // Also the polygon has a max of 1000 vertices.
            // And raster mask extraction is probably faster since the
            // polygon is converted internally to a grid anyway.
            IGeoDataset tClipped;
            if (pRasterToClip as IRaster != null)
            {
                logger.LogMessage(ServerLogger.msgType.debug, "SummariseRaster", 99,
                    "Input was in raster form, using directly to clip...");
                tClipped = tExtractionOp.Raster(pRasterToClip, pClippingPolygon);
            }
            else
            {
                // POLYGON VERSION: tExtractionOp.Polygon(pClipRaster,pPolygon,true)
                // sometimes we need to be able to pass in a polygon but rather than using the polygon
                // method we'll manually convert to a mask raster to avoid the issues above
                // It would save work to do this once for each request rather than repeat the conversion
                // for each layer - but we might want a different environment (snap extent etc) for each.
                logger.LogMessage(ServerLogger.msgType.debug, "SummariseCategoricalRaster", 99,
                    "Converting input polygon to mask raster for clip...");
                IRasterConvertHelper tConvertPolygonToRaster = new RasterConvertHelperClass();
                // convert it to a raster with the same cell size as the input
                IRasterProps tRst = pRasterToClip as IRasterProps;
                IPnt tRstCellSize = tRst.MeanCellSize();
                double x = tRstCellSize.X;
                double y = tRstCellSize.Y;
                double cellSize = Math.Round(Math.Min(x, y), 0);
                object tCellSizeAsObjectForNoGoodReason = (System.Object)cellSize;
                tRasterAnalysisEnvironment.SetCellSize(esriRasterEnvSettingEnum.esriRasterEnvValue,
                    ref tCellSizeAsObjectForNoGoodReason);
                IGeoDataset tPolyAsRast =
                    tConvertPolygonToRaster.ToRaster1(pRasterToClip, "GRID", tRasterAnalysisEnvironment)
                as IGeoDataset;
                logger.LogMessage(ServerLogger.msgType.debug, "SummariseCategoricalRaster", 99,
                    "...done, proceeding with clip");
                tClipped = tExtractionOp.Raster(pRasterToClip, tPolyAsRast);
            }
            // now we have the clipped raster we need to summarise it differently depending on whether
            // we want a summary by category/value (for categorical rasters) or by stats (for float rasters)
            Dictionary<string, double> tResults;
            if (pCategoricalSummary)
            {
                tResults = WatershedDetailExtraction.SummariseRasterCategorically(tClipped);
                if (tResults.Count > 100)
                {
                    // sanity check: don't sum up more than 100 different values
                    tResults = WatershedDetailExtraction.SummariseRasterStatistically(tClipped);
                    pCategoricalSummary = false;
                }
            }
            else
            {
                tResults = WatershedDetailExtraction.SummariseRasterStatistically(tClipped);
            }
            tRasterAnalysisEnvironment.RestoreToPreviousDefaultEnvironment();
            return new RasterExtractionResult(pExtractionLayerConfig.ParamName, pCategoricalSummary, tResults);
            //return tResults;
        }
Ejemplo n.º 2
0
        public void Construct(IPropertySet props)
        {
            try
            {
                logger.LogMessage(ServerLogger.msgType.infoDetailed, "Construct", 8000, "Watershed SOE constructor running");
                object tProperty = null;
                m_CanDoWatershed = true;
                // IPropertySet doesn't have anything like a trygetvalue method
                // so if we don't know if a property will be present we have to just try getting
                // it and if there is an exception assumes it wasn't there
                try {
                    tProperty = props.GetProperty("FlowAccLayer");
                    if (tProperty as string == "None")
                    {
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: Flow accumulation layer set to 'None'. No watershed functionality.");
                        m_CanDoWatershed = false;
                        //throw new ArgumentNullException();
                    }
                    else
                    {
                        m_FlowAccLayerName = tProperty as string;
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: found definition for Flow Accumulation layer: " + m_FlowAccLayerName);
                    }
                }
                catch{
                    logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: Flow accumulation layer not set. No watershed functionality.");
                    m_CanDoWatershed = false;
                    //throw new ArgumentNullException();
                }
                try {
                    tProperty = props.GetProperty("FlowDirLayer");
                    if (tProperty as string == "None")
                    {
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: Flow direction layer set to 'None'. No watershed functionality.");
                        m_CanDoWatershed = false;
                    }
                    else
                    {
                        m_FlowDirLayerName = tProperty as string;
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: found definition for Flow direction layer: " + m_FlowDirLayerName);
                    }
                }
                catch {
                    logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: Flow direction layer not set. No watershed functionality.");
                    m_CanDoWatershed = false;
                }
                try
                {
                    tProperty = props.GetProperty("ExtentFeatureLayer") as string;
                    if (tProperty as string =="None"){
                        logger.LogMessage(ServerLogger.msgType.debug, "Construct", 8000,
                            "WSH: No extent features configured. Extent may still be passed as input");
                    }
                    else
                    {
                        m_ExtentFeatureLayerName = tProperty as string;
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: found definition for Extent Feature layer: " + m_ExtentFeatureLayerName);
                    }
                }
                catch {
                    logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: no definition for extent feature layers found. Extent may still be passed as input");
                }

                try
                {
                    tProperty = props.GetProperty("ReadConfigFromMap");
                    if (tProperty == null || tProperty as string != "False")
                    {
                        m_BuildLayerParamsFromMap = true;
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: layer parameters will be built from map document layers");
                    }

                    else
                    {
                        m_BuildLayerParamsFromMap = false;
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: layer parameters would be read from properties file but this is NOT IMPLEMENTED YET ");
                        // TODO: add code to read in LayerConfiguration parameter and parse it
                    }
                }
                catch
                {
                    m_BuildLayerParamsFromMap = true;
                    logger.LogMessage(ServerLogger.msgType.debug, "Construct", 8000,
                        "WSH: no property found for ReadConfigFromMap; "+
                        "layer parameters will be built from map document layers");
                }
            }
            catch (Exception e)
            {
                logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, "WSH: Properties constructor threw an exception");
                logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, e.Message);
                logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, e.ToString());
                logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, e.TargetSite.Name);
                logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000, e.StackTrace);
            }

            try
            {
                // get the datasets associated with the configured inputs to watershed delineation.
                // Also note the other layers: we will make all others available for extraction
                // but need to note the data type and how the layer name should translate into a REST operation
                // parameter. This information will be stored in an ExtractionLayerConfig opbject for each layer
                // We only need to do this at startup not each time
                IMapServer3 mapServer = (IMapServer3)serverObjectHelper.ServerObject;
                string mapName = mapServer.DefaultMapName;
                IMapLayerInfo layerInfo;
                IMapLayerInfos layerInfos = mapServer.GetServerInfo(mapName).MapLayerInfos;
                ILayerDescriptions layerDescriptions = mapServer.GetServerInfo(mapName).DefaultMapDescription.LayerDescriptions;
                IMapServerDataAccess dataAccess = (IMapServerDataAccess)mapServer;

                int c = layerInfos.Count;
                int acc_layerIndex=0;
                int dir_layerIndex=0;
                int ext_layerIndex=0;
                //Dictionary<int,string> other_layerIndices = new Dictionary<int,string>();
                List<string> tAllParams = new List<string>();
                for (int i=0;i<c;i++)
                {
                    layerInfo = layerInfos.get_Element(i);
                    if(m_CanDoWatershed && layerInfo.Name == m_FlowAccLayerName)
                    {
                        acc_layerIndex = i;
                    }
                    else if (m_CanDoWatershed && layerInfo.Name == m_FlowDirLayerName)
                    {
                        dir_layerIndex = i;
                    }
                    else if (m_CanDoWatershed && m_ExtentFeatureLayerName != null && layerInfo.Name == m_ExtentFeatureLayerName)
                    {
                        ext_layerIndex = i;
                    }
                    else if (m_BuildLayerParamsFromMap)
                    // note the else if is deliberately arranged so that layers used for watershed extraction
                    // won't be exposed as extractable
                    {
                        // Types appear to be "Raster Layer", "Feature Layer", and "Group Layer"
                        logger.LogMessage(ServerLogger.msgType.debug,
                            "Construct", 8000,
                            "WSH: processing extractable map layer " + layerInfo.Name + " at ID " +
                            layerInfo.ID + " of type " + layerInfo.Type);

                        if (layerInfo.Type == "Raster Layer" || layerInfo.Type == "Feature Layer")
                        {
                            string tName = layerInfo.Name;
                            string tDesc = layerInfo.Description;
                            if (tName.IndexOf(':') == -1 && tDesc.IndexOf(':') == -1)
                            {
                                // fail if any of the map layers except the ones used for the catchment definition
                                // don't have a name or description starting with 6 or less characters followed by :
                                logger.LogMessage(ServerLogger.msgType.error, "Construct", 8000,
                                     " Watershed SOE warning: could determine output parameter string for layer " + tName +
                                     " and it will not be available for extraction. " +
                                     " Ensure that either the layer name or description starts with an ID for the " +
                                     " service parameter name to be exposed, max 6 characters and separated by ':'" +
                                     " e.g. 'LCM2K:Land Cover Map 2000'");
                                continue;
                            }
                            else if (tName.IndexOf(':') > 5 && tDesc.IndexOf(':') > 5)
                            {
                                logger.LogMessage(ServerLogger.msgType.error, "Construct", 8000,
                                     " Watershed SOE warning: read output parameter string for layer " + tName +
                                     " but it was too long." +
                                     " Ensure that either the layer name or description starts with an ID for the " +
                                     " service parameter name to be exposed, max 6 characters and separated by ':'" +
                                     " e.g. 'LCM2K:Land Cover Map 2000'. Layer will not be available for extraction.");
                                continue;
                            }
                            string tParamName;
                            string tProcessedName;
                            if (tName.IndexOf(':') != -1)
                            {
                                tParamName = tName.Substring(0, tName.IndexOf(':'));
                                tProcessedName = tName.Substring(tName.IndexOf(':')+1).Trim();
                            }
                            else
                            {
                                tParamName = tDesc.Substring(0, tDesc.IndexOf(':'));
                                tProcessedName = tName.Trim();
                            }
                            if (tAllParams.Contains(tParamName))
                            {
                                logger.LogMessage(ServerLogger.msgType.error,"Construct",800,
                                    "Watershed SOE warning: duplicate parameter name found for layer "+tName +
                                    "(parameter "+tParamName+" is set on another map layer). Layer will not be available"+
                                    " for extraction.");
                                continue;
                            }
                            else{
                                tAllParams.Add(tParamName);
                            }
                            string tDescription = "";
                            if (layerInfo.Description.Length > 0)
                            {
                                if (layerInfo.Description.IndexOf(':') == -1)
                                {
                                    tDescription = layerInfo.Description.Trim();
                                }
                                else if (layerInfo.Description.IndexOf(':') < 6)
                                {
                                    tDescription = layerInfo.Description.Substring(layerInfo.Description.IndexOf(':') + 1).Trim();
                                }
                                else
                                {
                                    tDescription = layerInfo.Description.Trim();
                                }
                            }

                            ExtractionTypes tExtractionType = ExtractionTypes.Ignore;
                            if (layerInfo.Type == "Raster Layer")
                            {
                                // determine whether we will summarise the raster layer "categorically"
                                // i.e. a count of each value, or "continuously" i.e. min/max/avg statistics
                                // based on how the raster is symbolised in the map and whether or not
                                // it is of integer type

                                // TODO : Also store the labels for classes in categorical rasters
                                // so that these can be returned by the SOE to the client
                                // Cast the renderer to ILegendInfo, get ILegendGroup from it and each
                                // ILegendClass from that to get the string Label

                                // Get renderer
                                // THIS ONLY WORKS WITH MXD SERVICES: WE CANNOT DO THIS ON AN MSD BASED SERVICE
                                IMapServerObjects3 tMapServerObjects = mapServer as IMapServerObjects3;
                                ILayer tLayer = tMapServerObjects.get_Layer(mapName, i);
                                IRasterLayer tRasterLayer = (IRasterLayer)tLayer;
                                IRasterRenderer tRasterRenderer = tRasterLayer.Renderer;
                                // Get raster data
                                IRaster tRaster = dataAccess.GetDataSource(mapName, i) as IRaster;
                                IRasterProps tRasterProps = tRaster as IRasterProps;
                                IGeoDataset tRasterGDS = tRaster as IGeoDataset;

                                bool tTreatAsCategorical = false;
                                if (tRasterRenderer is RasterUniqueValueRenderer)
                                {
                                    logger.LogMessage(ServerLogger.msgType.debug, "Construct", 800,
                                   "Raster layer " + tName +
                                   "is symbolised by unique values - treating layer as categorical");
                                    tTreatAsCategorical = tRasterProps.IsInteger;
                                }
                                else if (tRasterRenderer is RasterDiscreteColorRenderer)
                                {
                                    logger.LogMessage(ServerLogger.msgType.debug, "Construct", 800,
                                   "Raster layer " + tName +
                                   "is symbolised by discrete colours - treating layer as categorical");
                                    tTreatAsCategorical = tRasterProps.IsInteger;
                                }
                                else if (tRasterRenderer is RasterClassifyColorRampRenderer)
                                {
                                    // TODO - treat a classified colour ramp as categorical but categories
                                    // determined by classes rather than unique values... needs the summary
                                    // method to have access to the class breaks
                                    logger.LogMessage(ServerLogger.msgType.debug, "Construct", 800,
                                   "Raster layer " + tName +
                                   "is symbolised by classified groups - treating layer as continuous");
                                }
                                else if (tRasterRenderer is RasterStretchColorRampRenderer)
                                {
                                    logger.LogMessage(ServerLogger.msgType.debug, "Construct", 800,
                                   "Raster layer " + tName +
                                   "is symbolised by colour stretch - treating layer as continuous");
                                }
                                else
                                {
                                    logger.LogMessage(ServerLogger.msgType.debug, "Construct", 800,
                                   "Raster layer " + tName +
                                   "is symbolised with unsupported renderer - treating as continuous");
                                }
                                tExtractionType = tTreatAsCategorical?
                                    ExtractionTypes.CategoricalRaster:
                                    ExtractionTypes.ContinuousRaster;
                                ExtractionLayerConfig tLayerInfo = new ExtractionLayerConfig
                                    (i, tProcessedName,tDescription,tExtractionType, tParamName, -1, -1, -1,tRasterGDS);
                                m_ExtractableParams.Add(tLayerInfo);
                            }
                            else
                            {
                                // Feature class layer
                                // TODO - Get the category / values from the symbology (renderer) as for rasters

                                IFeatureClass tFC = dataAccess.GetDataSource(mapName, i) as IFeatureClass;
                                IGeoDataset tFeatureGDS = tFC as IGeoDataset;
                                esriGeometryType tFCType = tFC.ShapeType;
                                if (tFCType == esriGeometryType.esriGeometryPoint || tFCType == esriGeometryType.esriGeometryMultipoint)
                                {
                                    tExtractionType = ExtractionTypes.PointFeatures;
                                }
                                else if (tFCType == esriGeometryType.esriGeometryPolyline || tFCType == esriGeometryType.esriGeometryLine)
                                {
                                    tExtractionType = ExtractionTypes.LineFeatures;
                                }
                                else if (tFCType == esriGeometryType.esriGeometryPolygon)
                                {
                                    tExtractionType = ExtractionTypes.PolygonFeatures;
                                }
                                int tCategoryField = layerInfo.Fields.FindFieldByAliasName("CATEGORY");
                                int tValueField = layerInfo.Fields.FindFieldByAliasName("VALUE");
                                int tMeasureField = layerInfo.Fields.FindFieldByAliasName("MEASURE");
                                ExtractionLayerConfig tLayerInfo = new ExtractionLayerConfig
                                    (i,tProcessedName,tDescription, tExtractionType, tParamName, tCategoryField, tValueField, tMeasureField,tFeatureGDS);
                                m_ExtractableParams.Add(tLayerInfo);
                                // layers with any other geometry type will be ignored
                            }
                        }
                    }
                    else
                    {
                        logger.LogMessage(ServerLogger.msgType.infoStandard, "Construct", 8000,
                            "WSH: Code to build layer params from properties is not implemented. No extractable"+
                            "params will be available.");
                    }
                }
                IRaster tFDR = dataAccess.GetDataSource(mapName,dir_layerIndex) as IRaster;
                m_FlowDirDataset =  tFDR as IGeoDataset;
                IRaster tFAR = dataAccess.GetDataSource(mapName,acc_layerIndex) as IRaster;
                m_FlowAccDataset = tFAR as IGeoDataset;
                if(m_FlowDirDataset == null || m_FlowAccDataset == null)
                {
                    logger.LogMessage(ServerLogger.msgType.error,"Construct", 8000,"Watershed SOE Error: layer not found");
                    m_CanDoWatershed = false;
                   // return;
                }
                else
                {
                    m_CanDoWatershed = true;
                }
                if (ext_layerIndex != 0)
                {
                    m_ExtentFeatureDataset = dataAccess.GetDataSource(mapName, ext_layerIndex) as IGeoDataset;
                }
            }
            catch (Exception e)
            {
                logger.LogMessage(ServerLogger.msgType.error,"Construct",8000,"Watershed SOE error: could not get the datasets associated with configured map layers."+
                    "Exception: "+e.Message+e.Source+e.StackTrace+e.TargetSite);
            }
            try
            {
                reqHandler = new SoeRestImpl(soe_name, CreateRestSchema()) as IRESTRequestHandler;
            }
            catch (Exception e)
            {
                logger.LogMessage(ServerLogger.msgType.error, "Construct", 8000, "WSH: could not create REST schema. Exception: "+e.Message+ " "+e.Source+" "+e.StackTrace+" "+e.TargetSite);

            }
        }
        //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);
            }
        }