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