ProConcepts Parcel Fabric - Esri/arcgis-pro-sdk GitHub Wiki
The parcel fabric is a comprehensive framework of functionality in ArcGIS for modeling parcels in organizations ranging from national cadastral agencies to local governments. This topic provides an introduction to the parcel fabric API. It details the classes and methods that query and edit the parcel fabric. The parcel fabric API is commonly used in conjunction with the geodatabase and editing APIs.
Language: C#
Subject: Parcel Fabric
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 10/06/2024
ArcGIS Pro: 3.4
Visual Studio: 2022
-
The parcel fabric API is a Data Manipulation Language (DML)-only API. This means that all schema creation and modification operations such as creating parcel types or adding and deleting rules need to use the geoprocessing API.
-
Almost all of the methods in the parcel fabric API should be called on the Main CIM Thread (MCT). The API reference documentation on the methods that need to run on the MCT are specified as such. These method calls should be wrapped inside the QueuedTask.Run call. Failure to do so will result in ConstructedOnWrongThreadException being thrown. See Working with multi-threading in ArcGIS Pro to learn more.
-
In a multi-user environment a parcel fabric is accessed via services and not client-server. As such the parcel fabric API is designed to support a service-oriented architecture. The parcel fabric also has a single-use model that works directly on a file geodatabase.
Note: This topic assumes a basic understanding of the parcel fabric information model. See the online help for more information. See: What is the parcel fabric?
The items of the parcel fabric are included in the ArcGIS.Desktop.Editing
and ArcGIS.Desktop.Mapping
assembly namespaces.
Add using ArcGIS.Desktop.Editing;
and using ArcGIS.Desktop.Mapping;
to the top of your source files.
The parcel fabric functionality is available at the Standard
and Advanced
license levels. Code that makes function calls to the Parcel Fabric API should check license level on the client application to avoid an Insufficient License
exception. Check license level prior to making these function calls. In general, the try{} catch{}
pattern should also be used to handle exceptions such as these. However, an explicit license check can be used as an early confirmation.
This code returns the current license level of the application and returns a message if the license is at the Basic
level:
var lic = ArcGIS.Core.Licensing.LicenseInformation.Level;
if (lic < ArcGIS.Core.Licensing.LicenseLevels.Standard)
{
MessageBox.Show("Insufficient License Level.");
return;
}
The parcel fabric is a controller dataset that handles a set of simple feature classes, a single geodatabase topology, and a set of attribute rules. Multiple parcel types can be added to a fabric. Each parcel type is represented by a polygon-polyline featureclass-pair. Each parcel typeβs featureclass-pair has its own schema and can be extended with additional fields, domains and subtypes. Validating a parcel fabricβs topology rules and evaluating attribute rules may result in the creation of error features that report on rules that are outside their defined limits. To learn more see the help topic Parcel fabric data model.
The ParcelFabric
class provides an abstraction of the controller dataset. Methods on this class provide an entry point to the other areas of the parcel fabric API, including the topology, the system tables, the definition, and the parcel type information.
As with other datasets in the geodatabase, a ParcelFabric object can be obtained by calling Geodatabase.OpenDataset()
.
ParcelFabric objects can also be obtained from a table or feature class that are controlled by the parcel fabric by using Table.GetControllerDatasets()
.
Note that a particular parcel feature will belong to both controller datasets, the topology as well as the parcel fabric.
The Geodatabase.OpenDataset()
routine takes the name of a dataset to open. When using feature services, the name of the dataset in the feature service workspace is typically obtained from the corresponding definition object.
The following code shows how to access the parcel fabric from its REST end-point:
static void Main(string[] args)
{
Host.Initialize();
Uri service = new("https://myserver.esri.com/server/rest/services/myFeatureServiceName/FeatureServer");
ServiceConnectionProperties serviceConnectionProperties = new(service);
Uri portal_uri = new("https://myserver.esri.com/portal/");
ArcGIS.Core.SystemCore.ArcGISSignOn.Instance.SignInWithCredentials(
portal_uri, "admin", "adminpassword", out _, out _);
if (serviceConnectionProperties != null)
{
//connect
using var featService = new Geodatabase(serviceConnectionProperties);
ParcelFabric myFabric = featService.OpenDataset<ParcelFabric>("L0MyParcelFabric");
var schemaVersion = myFabric.GetDefinition().GetSchemaVersion();
}
}
A ParcelFabric object can be obtained from a table as follows:
public static ParcelFabric GetParcelFabricFromTable(Table table)
{
ParcelFabric parcelFabric = null;
if (table.IsControllerDatasetSupported())
{
// Tables can belong to multiple controller datasets,
// but at most one of them will be a parcel fabric
IReadOnlyList<Dataset> controllerDatasets = table.GetControllerDatasets();
foreach (Dataset controllerDataset in controllerDatasets)
{
if (controllerDataset is ParcelFabric)
{
parcelFabric = controllerDataset as ParcelFabric;
}
else
{
controllerDataset.Dispose();
}
}
}
return parcelFabric;
}
Note that the parcel fabric controller dataset can also be obtained directly from the ParcelLayer as described in the section Parcel layer.
The IsSystemTableSupported()
method on the ParcelFabric class can be used to determine if the given system table exists (certain tables may not be available depending on the version of the parcel fabric schema). These tables are then available by using the GetSystemTable()
method.
That code is written as:
if (myParcelFabricDataset.IsSystemTableSupported(SystemTableType.AdjustmentPoints))
var adjPointTable = myParcelFabricDataset.GetSystemTable(SystemTableType.AdjustmentPoints);
The ParcelFabricDefinition class provides metadata information about the parcel fabric. Get the schema version for the fabric as follows:
var pfDefinition = myParcelFabricDataset.GetDefinition();
string msg = "Parcel Fabric Schema Version: " + pfDefinition.GetSchemaVersion();
You can determine if the parcel fabric's topology is enabled, and then get the topology controller dataset used with the parcel fabric as follows:
Topology topoDS = null;
var pfDefinition = myParcelFabricDataset.GetDefinition();
if (pfDefinition.GetTopologyEnabled())
topoDS = myParcelFabricDataset.GetParcelTopology();
The ParcelTypeInfo object returns the data related to the parcel types. The following code retrieves the type name, and the names of the line and polygon feature classes:
string msg = "Types\n\n";
var typeInfo = myParcelFabricDataset.GetParcelTypeInfo();
foreach (var info in typeInfo)
msg += "Name:" + info.Name + "\n L:" + info.LineFeatureTable.GetName() + "\n P:" + info.PolygonFeatureTable.GetName() + "\n\n";
Only one parcel layer can be added to a map view. Attempting to add a second parcel layer to a map view through the user interface or through code is prevented by the system, and an error is returned. The following code adds a parcel fabric to the active map view:
protected async override void OnClick()
{
string path = @"C:\MyTestData\MyFileGeodatabase.gdb\MyFeatureDS\MyFabric";
await QueuedTask.Run(() =>
{
var lyrCreateParams = new ParcelLayerCreationParams(new Uri(path));
try
{
var parcelLayer = LayerFactory.Instance.CreateLayer<ParcelLayer>(
lyrCreateParams, MapView.Active.Map);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Add Parcel Fabric Layer");
}
});
}
Hence there is a reasonable expectation that there will only be one fabric layer in the map view. Consequently the following line of code is reliable for accessing the parcel layer.
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
When the fabric layer is added to the active map view using the code above, the associated parcel layers are, by default, preconfigured with their expected role to be for parcel editing. The layers can also be added to the map with the intent that they are to be published. For publication, the layers have different requirements than for editing. For example, the layers must not have any definition queries and there cannot be repeated feature layers pointing to the same data source.
To programmatically add a parcel layer for publication, the code above would be written to include a line to assign the layer creation role:
var lyrCreateParams = new ParcelLayerCreationParams(new Uri(path));
lyrCreateParams.LayerCreationRole = ParcelLayerCreationRole.ParcelPublicationLayers;
The parcel fabric controller dataset can be obtained from the ParcelLayer as follows:
ParcelFabric parcelFabric = myParcelFabricLayer.GetParcelFabric();
The feature layers for parcel fabric points, connections, records, and parcel types, can be tested for their source feature classes being controlled by a parcel fabric.
You can check if a feature layer is controlled by a fabric as follows:
// Check if a layer has a parcel fabric source
var myFeatureLayer = MapView.Active.GetSelectedLayers().FirstOrDefault();
bool bIsControlledByFabric =
await myFeatureLayer.IsControlledByParcelFabricAsync(ParcelFabricType.ParcelFabric);
NOTE: There are two different types of parcel fabrics, the type created by ArcMap, and the new type created when using ArcGIS Pro. In ArcGIS Pro the parcel fabrics from ArcMap are read-only. You can test if a layer's source is pointing to a legacy ArcMap parcel fabric by using the following code:
// Check if a layer has a source that is a parcel fabric for ArcMap
var myFeatureLayer = MapView.Active.GetSelectedLayers().FirstOrDefault();
bool bIsControlledByArcMapFabric =
await myFeatureLayer.IsControlledByParcelFabricAsync(ParcelFabricType.ParcelFabricForArcMap);
To learn more about upgrading an ArcMap parcel fabric to work in ArcGIS Pro see Upgrade an ArcMap parcel fabric.
Coordinate Geometry (COGO) is a feature editing technique used primarily in the land records industry. All the line features found in the parcel fabric are automatically COGO Enabled. This means that the lines have a well known schema using predefined fields for storing dimension information like directions and distances. This is a broad topic that is important for working with the lines in the parcel fabric. For more information about how to use this area of the API see the topic called ProConcepts COGO.
The fabric's parcel layer has extension methods to get and set the active record. Note that nearly all the methods in the parcel fabric API should be called on the Main CIM Thread (MCT). The API reference documentation on the methods indicate those that need to run on the MCT. These method calls should be wrapped inside the QueuedTask.Run call. Failure to do so will result in ConstructedOnWrongThreadException being thrown. See Working with multi-threading in ArcGIS Pro to learn more.
When editing a parcel fabric there are two high-level conceptual workflow modes:
- Record driven workflows
- Quality driven workflows
Record driven workflows are those that update the legal parcel information and the parcel lineage; the parent parcel is retired when its child parcel(s) are created. This happens when entering data from a legal land record document such as a Deed, Subdivision Plan, Record of Survey, and so on. For example, when an existing parcel gets subdivided into two portions the legal document that defines this land transaction is called the legal record. The original parent parcel is retired by that record and the two new parcels are created by that record. From the user workflow perspective this requires that the first step is to create the record in the fabric and then to set that new record as active. Here is code to do that:
string errorMessage = await QueuedTask.Run( async () =>
{
Dictionary<string, object> RecordAttributes = new Dictionary<string, object>();
string sNewRecord = "MyRecordName";
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
//if there is no fabric in the map then bail
if (myParcelFabricLayer == null)
return "Please add a parcel fabric to the map.";
try
{
var recordsLayer = await myParcelFabricLayer.GetRecordsLayerAsync();
var editOper = new EditOperation()
{
Name = "Create Parcel Fabric Record",
ProgressMessage = "Create Parcel Fabric Record...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = false,
SelectModifiedFeatures = false
};
RecordAttributes.Add("Name", sNewRecord);
editOper.Create(recordsLayer.FirstOrDefault(), RecordAttributes);
if (!editOper.Execute())
return editOper.ErrorMessage;
await myParcelFabricLayer.SetActiveRecordAsync(sNewRecord);
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage);
The active recordβs globally unique identifier (guid) is used to populate the fields called RetiredByRecord
and CreatedByRecord
. These fields are populated when new parcel features are created or when existing parcels are retired and are being replaced by new parcels.
The concept of a record being active is client specific. The server does not persist any information related to a record being active. The client has a state whereas the server is stateless.
var pRec = myParcelFabricLayer.GetActiveRecord();
When the active record is set the edits that create new parcel features are tagged with this active recordβs guid and edits like Merge, Divide, Build will apply the correct parcel edits and parcel lineage edits. In the following code note that there are no parcel API function calls. However, when this code is editing features controlled by a fabric these enhanced parcel edits are applied if there is an active record. When the active record is not set the edits are standard edits and the enhanced parcel edit behavior is not used. Therefore if you need to turn off the parcel editing behavior, your code should de-activate the active record.
myParcelFabricLayer.ClearActiveRecord();
The following code will Merge selected features and will first check to ensure that there is an active record since in this example we'd like the parcel lineage edits to be applied:
string errorMessage = await QueuedTask.Run( async () =>
{
// check for selected layer
if (MapView.Active.GetSelectedLayers().Count == 0)
return "Please select a feature layer in the table of contents.";
//get the feature layer that's selected in the table of contents
var featSrcLyr= MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().FirstOrDefault();
if (featSrcLyr.SelectionCount == 0)
return "There is no selection on the source layer.";
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
string sTargetParcelType = "Tax";
if (sTargetParcelType.Trim().Length == 0)
return "";
try
{
var typeNamesEnum =
await myParcelFabricLayer.GetParcelPolygonLayerByTypeNameAsync(sTargetParcelType);
if (typeNamesEnum.Count() == 0)
return "Target parcel type " + sTargetParcelType + " not found. Please try again.";
var featTargetLyr = typeNamesEnum.FirstOrDefault();
var pRec = myParcelFabricLayer.GetActiveRecord();
if (pRec == null)
return "There is no Active Record. Please set the active record and try again.";
var opMerge = new EditOperation()
{
Name = "Merge",
ProgressMessage = "Merging parcels...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
opMerge.Merge(featTargetLyr, featSrcLyr, featSrcLyr.GetSelection().GetObjectIDs());
opMerge.Execute();
}
catch (Exception ex)
{
return ex.Message;
}
return "";
//When active record is set, and merging into the same parcel type, the original parcels
//are set historic with RetiredByRecord GUID field
//When active record is set, the new merged parcel will be tagged with the Active record.
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage);
For another example and further information on parcel behavior triggered by the active record see the section below Integration with the Editing API.
You can write your code to check if there is an active record and if necessary prompt for it to be set. Here is code that does that:
// Check if there is an active record. If no active record then prompt user to set it or create it.
// Otherwise, warn user that new features will not have an active record.
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
var pRec = myParcelFabricLayer.GetActiveRecord();
if (pRec == null)
{
System.Windows.MessageBox.Show("There is no Active Record. Please set the active record and try again.", "Merge Parcels");
return;
}
The quality driven workflows are used for things like attribute edits such as correcting a parcel name, its stated area, or the attributed distance on a parcelβs boundary line. These quality driven workflows can also be geometry based such as re-aligning parcel boundaries. Quality driven workflows do not require an active record to be set and if an active record is set for the map, it will have no impact on the quality driven workflows.
The fabric has user-defined parcel types. Each parcel type is represented by a polygon feature class and a polyline feature class. A parcel type is created with a given name such as 'Tax' or 'Lot', for example, and the parcel type is represented in the fabric as a polygon feature class and polyline feature class.
If polygons in a parcel type exist without corresponding lines around its perimeter, then you use a Build function to create the lines for the parcels. Similarly, if there are lines in a parcel type that form loops but that do not enclose a parcel, then you can build parcel polygons from the parcel lines. The fabric also has points. The points are shared between parcel types so a parcelβs points do not have a parcel type. Points are also assigned to the active record when they are created.
The list of parcel types controlled by a parcel fabric can be returned through the following code:
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
IEnumerable<string> parcelTypeNames = await myParcelFabricLayer.GetParcelTypeNamesAsync();
In the map view a parcel type is represented by a line and polygon feature layer. By default these are presented in a group layer in the table of contents. There is also a sub-group layer called Historic that holds the historic lines and historic parcels for the same type. The historic layers are used to show the parcel features that have been retired by a later record in the parcel lineage.
In addition to being able to check if a given feature layer is controlled by a fabric, as described in the Parcel layer section above, you can also get the feature layer for a particular parcel type. For example, the following code returns the polygon feature layer for a parcel type called 'Lot' :
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
var polygonLyrEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeNameAsync("Lot");
if (polygonLyrEnum.Count() == 0)
return;
var myLotParcelPolygonLyr = polygonLyrEnum.FirstOrDefault();
Note that the code above returns an IEnumerable
for the polygon layer. The reason for this is to account for the possibility that the map view has multiple instances of feature layers for the same parcel type. However, this is not expected to be a common situation; the polygon and the line feature layers are most typically represented as a single instance for each within their respective type groups.
Conceptually, seeds are the preliminary geometries for parcels. The seeds are used to help the user work with the parcel by maintaining the parcel's attributes in a compact geometry that is easy to move and place while in its un-built state. A seed is usually a small circular polygon geometry that is inside a closed loop of lines.
A seed is a row in the feature class table of a parcel typeβs polygon feature class.
You can modify the attributes of the parcel seed or move it to a different closed loop of lines. During the build operation the geometry of the seed is modified to become a topologically complete parcel that fills the space enclosed by the parcel lines.
The following code reduces an existing parcel back into a seed and, as part of the same edit operation, alters the original parcel name:
var MyParcelEditToken = editOper.ShrinkParcelsToSeeds(myParcelFabricLayer,
SelectionSet.FromDictionary(sourceFeatures));
editOper.Execute();
var editOperation2 = editOper.CreateChainedOperation();
Dictionary<string, object> ParcelAttributes = new Dictionary<string, object>();
var FeatSetModified = MyParcelEditToken.ModifiedFeatures;
string sReportResult = "";
if (FeatSetModified != null)
{
foreach (KeyValuePair<MapMember, List<long>> kvp in FeatSetModified.ToDictionary())
{
foreach (long oid in kvp.Value)
{
var dictInspParcelFeat = kvp.Key.Inspect(oid).ToDictionary(a => a.FieldName, a => a.CurrentValue);
ParcelAttributes.Add("Name", "Seed for: " + dictInspParcelFeat["Name"]);
editOperation2.Modify(kvp.Key, oid, ParcelAttributes);
ParcelAttributes.Clear();
}
sReportResult += "There were " + kvp.Value.Count().ToString() + " " +
kvp.Key.Name + " features modified." + Environment.NewLine;
}
}
if (editOperation2.Execute())
return sReportResult;
Note that the ParcelEditToken
is used in the code above. More information about the ParcelEditToken
follows in the section Using a parcel edit token.
There is a typical order for parcel data creation when using record driven workflows. It is not a strictly enforced requirement, but in general there should first be a record feature created, and then that record should be set active before creating parcel seeds, or before creating parcel lines or points. Here is an example of a typical workflow:
- Create Record β this is making a new Record feature in the Records feature class, and setting it as the active record.
- Copy lines from an existing selection of non-fabric features into a parcel type called βLotβ in a parcel fabric. Parcel seeds are created automatically.
- Build the active record β this fills out the parcel typeβs polygons by "growing" the seeds, and also creates fabric points at the shared nodes.
- Create another new record, and set it active.
- Copy lines from an existing selection of βLotβ parcels into a parcel type called βTaxβ in the same parcel fabric. Parcel seeds are created automatically.
- Build the active record.
The current active record can be accessed as follows:
protected async override void OnClick()
{
string errorMessage = await QueuedTask.Run( () =>
{
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
var theActiveRecord = myParcelFabricLayer.GetActiveRecord();
if (theActiveRecord == null)
return "There is no Active Record. Please set the active record and try again.";
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage);
}
A record with a given name can be found and set as the active record as follows:
string errorMessage = await QueuedTask.Run( async () =>
{
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
//if there is no fabric in the map then bail
if (myParcelFabricLayer == null)
return "Please add a parcel fabric to the map.";
string sExistingRecord = "MyRecordName";
if (!await myParcelFabricLayer.SetActiveRecordAsync(sExistingRecord))
{
myParcelFabricLayer.ClearActiveRecord();
return "Record with that name not found.";
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage);
A new parcel fabric record can be created and set as the active record as follows:
string errorMessage = await QueuedTask.Run( async () =>
{
Dictionary<string, object> RecordAttributes = new Dictionary<string, object>();
string sNewRecord = "MyRecordName";
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
//if there is no fabric in the map then bail
if (myParcelFabricLayer == null)
return "Please add a parcel fabric to the map.";
try
{
var recordsLayer = await myParcelFabricLayer.GetRecordsLayerAsync();
var editOper = new EditOperation()
{
Name = "Create Parcel Fabric Record",
ProgressMessage = "Create Parcel Fabric Record...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = false,
SelectModifiedFeatures = false
};
RecordAttributes.Add("Name", sNewRecord);
editOper.Create(recordsLayer.FirstOrDefault(), RecordAttributes);
if (!editOper.Execute())
return editOper.ErrorMessage;
await myParcelFabricLayer.SetActiveRecordAsync(sNewRecord);
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage);
The editing assembly provides an event that gets fired when the active record is changed. Add-ins can subscribe to the ArcGIS.Desktop.Editing.Events.ParcelRecordEventArgs
to be notified when the active record is changed in the heads-up display of the active map. The ParcelRecordEventArgs
event argument passes the record objects that are involved when switching the active record.
Every add-in creates a module that you can add custom code to, and is useful for setting up event listeners that implement custom behavior. Confirm that AutoLoad
is set to true
in the module configuration found in the Config.daml file.
<modules>
<insertModule id="GroundToGridFromActiveRecord_Module" className="Module1" autoLoad="true" caption="Module1">
In the code below, an event listener is set up when the module loads.
internal Module1()
{
ActiveParcelRecordChangingEvent.Subscribe(ActiveRecordEventMethod);
}
~Module1()
{
ActiveParcelRecordChangingEvent.Unsubscribe(ActiveRecordEventMethod);
}
private async void ActiveRecordEventMethod(ParcelRecordEventArgs e)
{
var theNewActiveRecord = e.IncomingActiveRecord;
var newRecName = theNewActiveRecord?.Name;
var thePreviousActiveRecord = e.OutgoingActiveRecord;
var prevRecName = thePreviousActiveRecord?.Name;
if (newRecName == null && prevRecName == null)
return;
//Write custom code when the user changes the active record.
:
:
Note that the event argument has two methods; the IncomingActiveRecord
that returns the record that is made active with this event, and the OutgoingActiveRecord
that returns the record that had been active prior to this event. Either of these returned record objects can be null, and so they should be tested for null
before they are used.
The active record change event may be used to automatically apply other settings. For example, in the code below the ground to grid correction for the map is set automatically when the active record is changed. In this example the ground to grid values are read from the new active recordβs custom fields.
private async void ActiveRecordEventMethod(ParcelRecordEventArgs e)
{
var theNewActiveRecord = e.IncomingActiveRecord;
var newRecName = theNewActiveRecord?.Name;
var thePreviousActiveRecord = e.OutgoingActiveRecord;
var prevRecName = thePreviousActiveRecord?.Name;
if (newRecName == null && prevRecName == null)
return;
//Get the active map view
var mapView = MapView.Active;
if (mapView?.Map == null)
return;
//Get the fabric
var fabricLyr =
mapView.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (fabricLyr == null)
return;
//get the Records layer
var recordsLyr = fabricLyr.GetRecordsLayerAsync().Result.FirstOrDefault();
if (recordsLyr == null)
return;
if (theNewActiveRecord == null)
return;
List<long> lst = new ();
lst.Add(theNewActiveRecord.ObjectID);
var QueryFilter = new QueryFilter { ObjectIDs = lst.ToArray() };
var cim_g2g = await mapView.Map.GetGroundToGridCorrection();
// CIM for ground to grid is null for new maps,
// so initialize it for the first time here if needed.
if (cim_g2g == null)
cim_g2g = new CIMGroundToGridCorrection();
object oScaleFactor = null;
object oDirectionOffset = null;
using RowCursor rowCursor = recordsLyr.Search(QueryFilter);
while (rowCursor.MoveNext()) //should only be one record
{
//changing the G2G settings
var iFldDistanceFactor = rowCursor.FindField("distancefactor");
var iFldDirectionOffset = rowCursor.FindField("directionoffset");
if (iFldDistanceFactor == -1 && iFldDirectionOffset == -1)
return;
using Row rowRec = rowCursor.Current;
if (iFldDistanceFactor != -1)
{
oScaleFactor = rowRec[iFldDistanceFactor];
if (oScaleFactor != null)
{
cim_g2g.ConstantScaleFactor = Convert.ToDouble(oScaleFactor);
cim_g2g.Enabled = true;
cim_g2g.UseScale = true;
cim_g2g.ScaleType = GroundToGridScaleType.ConstantFactor;
}
else
cim_g2g.UseScale = false;
}
if (iFldDirectionOffset != -1)
{
oDirectionOffset = rowRec[iFldDirectionOffset];
if (oDirectionOffset != null)
{
cim_g2g.Direction = Convert.ToDouble(oDirectionOffset);
// store and set this in decimal degrees, irrespective of project unit settings
cim_g2g.Enabled = true;
cim_g2g.UseDirection = true;
}
else
cim_g2g.UseDirection = false;
}
}
if (oScaleFactor == null && oDirectionOffset == null)
return;//if both direction offset and scale are null, do nothing
await mapView.Map.SetGroundToGridCorrection(cim_g2g);
}
Adding new features to a parcel type's polygon feature class or to a parcel type's line feature class has specific behavior when a record is active. New parcel type features that are created will have the guid that is associated with the record stored in the parcel feature's CreatedByRecord
field. Similarly, parcel point features and connection line features will have the active record guid assigned when they are created by the standard editing API. This is done automatically and does not require additional code. The code below adds a parcel point but does not proceed with the edit until the user has specified an active record. This ensures that the parcel fabric behavior is in effect. The code snippet below could be enhanced to prompt the user to create or choose a record using code similar to that from the previous section, "Creating a new Record".
A new parcel point can be added to a fabric as follows:
string errorMessage = await QueuedTask.Run( async () =>
{
var lic = ArcGIS.Core.Licensing.LicenseInformation.Level;
if (lic < ArcGIS.Core.Licensing.LicenseLevels.Standard)
return "Insufficient License Level.";
//make sure there is a fabric and an active record
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
return "Please select a parcel fabric layer in the table of contents.";
try
{
var pRec = myParcelFabricLayer.GetActiveRecord();
if (pRec == null)
return "There is no Active Record. Please set the active record and try again.";
var pointLyrEnum = await myParcelFabricLayer.GetPointsLayerAsync();
if (pointLyrEnum.Count() == 0)
return "No point layer found.";
var pointLyr = pointLyrEnum.FirstOrDefault();
Dictionary<string, object> PointAttributes = new Dictionary<string, object>();
//using center of the map extent as the new point for purposes of this example
var newPoint = MapView.Active.Extent.Center;
if (newPoint == null)
return "";
PointAttributes.Add("Name", "MyTestPoint");
PointAttributes.Add("IsFixed", 1);
var editOper = new EditOperation()
{
Name = "Create a test parcel fabric point",
ProgressMessage = "Create a test parcel fabric point...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
editOper.Create(pointLyr, newPoint, PointAttributes);
if (!editOper.Execute())
return editOper.ErrorMessage;
}
catch (Exception ex)
{
errorMessage=ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Add Control Point");
Other editing functions will also apply parcel behavior when an active record has been set. For example the following code will merge selected features from a source layer into a target layer. This code is the same regardless of whether standard polygon features are being merged or if the features are a parcel type's polygon features. However, if the features are parcels and the active record is set, then parcel fabric behavior is applied. Note that the following code block does not have any parcel specific API functions. The parcel behavior that results is described further after the code block:
var opMerge = new EditOperation()
{
Name = "Merge",
ProgressMessage = "Merging parcels...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
opMerge.Merge(featLyrTarget, featLyrSource, featLyrSource.GetSelection().GetObjectIDs());
opMerge.Execute();
When the code above is executed on parcel polygon features and the source and target layers are different parcel types, and there is an active record, then the following attribute edits are applied automatically, without any additional code required:
- the newly created merged parcel has its
CreatedByRecord
field value updated with the active record's guid. - the newly created merged parcel has its StatedArea field value updated with the sum of the stated area values of the original selected parcels.
When the code above is executed on parcel polygon features and the source and target layers are the same parcel type, then parcel lineage is also automatically captured as follows:
- the existing source parcels have their
RetiredByRecord
field value updated with the active record's guid.
Parcels can be created in the fabric through code such as the approach in the example above for point creation. The parcel polygon feature and the parcel line features can be created directly in this way. The parcel fabric API also has additional methods for creating parcels. The following sections provide some code examples of these.
In addition to copying the source lines into the parcel type's line, the following code will also create parcel seeds within the detected closed-line loops:
editOper.CopyLineFeaturesToParcelType(srcStandardLineFeatureLayer, StandardLineObjectIds,
destParcelTypeLineLayer, destParcelTypePolygonLayer);
editOper.Execute();
In addition to copying the source parcel's lines into the target parcel type's line layer, the following code will also create parcel seeds at the centroid locations of the original source parcel polygons:
var ids = new List<long>(srcParcelFeatLyr.GetSelection().GetObjectIDs());
if (ids.Count == 0)
return "No selected parcels found. Please select parcels and try again.";
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(srcParcelFeatLyr, ids);
var editOper = new EditOperation();
editOper.CopyParcelLinesToParcelType(myParcelFabricLayer, SelectionSet.FromDictionary(sourceParcFeats),
destLineL, destPolygonL, true,false,true);
The last two parameters, useSourceLineAttributes
and useSourcePolygonAttributes
specify whether or not to copy across the values of extended attributes from the source features to the target features. The field mapping is done automatically based on name matching. The regular parcel fabric schema field values for attributes like direction and distance are always carried across regardless of how these last two parameters are set.
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
MessageBox.Show("Please add a parcel fabric to the map.", "Create Parcel Seeds");
return;
}
string errorMessage = await QueuedTask.Run( async () =>
{
try
{
var pRec = myParcelFabricLayer.GetActiveRecord();
if (pRec == null)
return "There is no Active Record. Please set the active record and try again.";
var guid = pRec.Guid;
var editOper = new EditOperation()
{
Name = "Create Parcel Seeds",
ProgressMessage = "Create Parcel Seeds...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
List<FeatureLayer> parcelLayers = new List<FeatureLayer>();
List<string> sParcelTypes = new List<string> { "Tax", "Lot" };
foreach (string sParcTyp in sParcelTypes)
{
IEnumerable<FeatureLayer> lyrs =
await myParcelFabricLayer.GetParcelPolygonLayerByTypeNameAsync(sParcTyp);
parcelLayers.Add(lyrs.FirstOrDefault());
lyrs = await myParcelFabricLayer.GetParcelLineLayerByTypeNameAsync(sParcTyp);
parcelLayers.Add(lyrs.FirstOrDefault());
}
editOper.CreateParcelSeedsByRecord(myParcelFabricLayer, guid, MapView.Active.Extent, parcelLayers);
if (!editOper.Execute())
return editOper.ErrorMessage;
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Create Parcel Seeds");
await QueuedTask.Run( () =>
{
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
return;
try
{
var theActiveRecord = myParcelFabricLayer.GetActiveRecord();
var guid = theActiveRecord.Guid;
var editOper = new EditOperation()
{
Name = "Build Parcels",
ProgressMessage = "Build Parcels...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = true
};
editOper.BuildParcelsByRecord(myParcelFabricLayer, guid);
editOper.Execute();
}
catch
{
return;
}
});
Also related to this code, note that it is possible to reduce parcels back to seeds. For more on this see topic below, Shrink parcels to seeds.
You can duplicate parcels from multiple source types into a single target parcel type. The following code example uses just a single parcel type as a source in the List
of KeyValuePair
. Additional KeyValuePair
items can be added to the List
for duplicating from additional source parcel types.
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
MessageBox.Show("Please add a parcel fabric to the map.", "Duplicate Parcels");
return;
}
string sTargetParcelType = "Tax";
string errorMessage = await QueuedTask.Run( async () =>
{
try
{
//get the feature layer that's selected in the table of contents
var sourceParcelTypePolygonLayer =
MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().FirstOrDefault();
string parcelPolygonType = sourceParcelTypePolygonLayer.Name; //assumes layer name matches parcel type name
var ids = new List<long>(sourceParcelTypePolygonLayer.GetSelection().GetObjectIDs());
if (ids.Count == 0)
return "No selected " + parcelPolygonType + " parcels found. Please select parcels and try again.";
var targetFeatLyrEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeNameAsync(sTargetParcelType);
if (targetFeatLyrEnum.Count()== 0)
return "No parcel type " + sTargetParcelType + " found.";
var targetFeatLyr = targetFeatLyrEnum.FirstOrDefault();
if (targetFeatLyr == null)
return "";
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(sourceParcelTypePolygonLayer, ids);
var pRec = myParcelFabricLayer.GetActiveRecord();
if (pRec == null)
return "There is no Active Record. Please set the active record and try again.";
var editOper = new EditOperation()
{
Name = "Duplicate Parcels",
ProgressMessage = "Duplicate Parcels...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
editOper.DuplicateParcels(myParcelFabricLayer, SelectionSet.FromDictionary(sourceParcFeats), pRec, targetFeatLyr);
if (!editOper.Execute())
return editOper.ErrorMessage;
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Duplicate Parcels");
Parcels can be directly updated in the fabric through code using the standard editing API. The parcel fabric API also has additional methods for updating parcels. The following sections and code snippets provide some examples of these.
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
MessageBox.Show("Please select a parcel fabric layer in the table of contents.","Assign Features To Record");
string errorMessage = await QueuedTask.Run( async () =>
{
//check for selected layer
if (MapView.Active.GetSelectedLayers().Count == 0)
return "Please select a source feature layer in the table of contents.";
//get the feature layer that's selected in the table of contents
var srcFeatLyr = MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().FirstOrDefault();
bool bIsControlledByFabric = await srcFeatLyr.IsControlledByParcelFabric(ParcelFabricType.ParcelFabric);
if (!bIsControlledByFabric)
return "Please select a parcel fabric layer in the table of contents.";
try
{
var pRec = myParcelFabricLayer.GetActiveRecord();
if (pRec == null)
return "There is no Active Record. Please set the active record and try again.";
var ids = new List<long>(srcFeatLyr.GetSelection().GetObjectIDs());
// ------- get the selected parcels ---------
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(srcFeatLyr, ids);
var editOper = new EditOperation()
{
Name = "Assign Features to Record",
ProgressMessage = "Assign Features to Record...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
editOper.AssignFeaturesToRecord(myParcelFabricLayer,
SelectionSet.FromDictionary(sourceParcFeats), pRec);
if (!editOper.Execute())
return editOper.ErrorMessage;
}
catch (Exception ex)
{
errorMessage= ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Assign Features To Record");
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
System.Windows.MessageBox.Show("There is no parcel layer in the map.", "Set Parcels Historic");
return;
}
string errorMessage = await QueuedTask.Run( async () =>
{
try
{
FeatureLayer destPolygonL = null;
//test to make sure the layer is a parcel type, is non-historic, and has a selection
bool bFound = false;
var ParcelTypesEnum = await myParcelFabricLayer.GetParcelTypeNames();
foreach (FeatureLayer mapFeatLyr in
MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>())
{
foreach (string ParcelType in ParcelTypesEnum)
{
var layerEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(ParcelType);
foreach (FeatureLayer flyr in layerEnum)
{
if (flyr == mapFeatLyr)
{
bFound = mapFeatLyr.SelectionCount > 0;
destPolygonL = mapFeatLyr;
break;
}
}
if (bFound) break;
}
if (bFound) break;
}
if (!bFound)
return "Please select parcels to set as historic.";
var theActiveRecord = myParcelFabricLayer.GetActiveRecord();
if (theActiveRecord == null)
return "There is no Active Record. Please set the active record and try again.";
var ids = new List<long>(destPolygonL.GetSelection().GetObjectIDs());
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(destPolygonL, ids);
var editOper = new EditOperation()
{
Name = "Set Parcels Historic",
ProgressMessage = "Set Parcels Historic...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
editOper.SetParcelHistoryRetired(myParcelFabricLayer, SelectionSet.FromDictionary(sourceParcFeats),
theActiveRecord);
if (!editOper.Execute())
return editOper.ErrorMessage;
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Set Parcels Historic");
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
MessageBox.Show("Please add a parcel fabric to the map.", "Change Parcel Type");
return;
}
string sSourceParcelType = "Tax";
string sTargetParcelType = "Lot";
string errorMessage = await QueuedTask.Run( async () =>
{
try
{
var sourcePolygonLotTypeEnum =
await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sSourceParcelType);
var sourcePolygonLotTypeLayer = sourcePolygonLotTypeEnum.FirstOrDefault();
Dictionary<string, object> RecordAttributes = new Dictionary<string, object>();
var recordsLayerEnum =
await myParcelFabricLayer.GetRecordsLayerAsync();
var recordsLayer = recordsLayerEnum.FirstOrDefault();
var targetParcelTypeFeatLyrEnum =
await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(sTargetParcelType);
var targetParcelTypeFeatLyr = targetParcelTypeFeatLyrEnum.FirstOrDefault();
if (myParcelFabricLayer == null || sourcePolygonLotTypeLayer == null)
return "";
var ids = new List<long>(sourcePolygonLotTypeLayer.GetSelection().GetObjectIDs());
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(sourcePolygonLotTypeLayer, ids);
var opCpToPT = new EditOperation()
{
Name = "Change Parcel Type",
ProgressMessage = "Change Parcel Type...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
opCpToPT.ChangeParcelType(myParcelFabricLayer, SelectionSet.FromDictionary(sourceParcFeats),
targetParcelTypeFeatLyr);
if (!opCpToPT.Execute())
return opCpToPT.ErrorMessage;
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Change Parcel Type");
As part of a quality driven workflow, it is useful to be able to reduce existing built parcels back down to seeds so that geometry edits can be made on the bounding lines without requiring topology maintenance on the geometry of the polygon as well. The parcel seeds are then re-built after the line geometry edits are done. The Shrink To Seeds command is used to convert existing parcels into seeds. The API for this function is demonstrated here:
string errorMessage = await QueuedTask.Run( async () =>
{
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
return "Please add a parcel fabric to the map.";
try
{
FeatureLayer parcelPolygonLyr = null;
//find the first layer that is a polygon parcel type, is non-historic, and has a selection
bool bFound = false;
var ParcelTypesEnum = await myParcelFabricLayer.GetParcelTypeNames();
foreach (FeatureLayer mapFeatLyr in
MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>())
{
foreach (string ParcelType in ParcelTypesEnum)
{
var layerEnum = await myParcelFabricLayer.GetParcelPolygonLayerByTypeName(ParcelType);
foreach (FeatureLayer flyr in layerEnum)
{
if (flyr == mapFeatLyr)
{
bFound = mapFeatLyr.SelectionCount > 0;
parcelPolygonLyr = mapFeatLyr;
break;
}
}
if (bFound) break;
}
if (bFound) break;
}
if (!bFound)
return "Please select parcels to shrink to seeds.";
var editOper = new EditOperation()
{
Name = "Shrink Parcels To Seeds",
ProgressMessage = "Shrink Parcels To Seeds...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
var ids = new List<long>(parcelPolygonLyr.GetSelection().GetObjectIDs());
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(sourcePolygonLotTypeLayer, ids);
editOper.ShrinkParcelsToSeeds(myParcelFabricLayer, SelectionSet.FromDictionary(sourceParcFeats));
if (!editOper.Execute())
return editOper.ErrorMessage;
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Shrink Parcels To Seeds");
After shrinking parcels to seeds one or more of the lines bounding a seed may not belong to the same record as that seed. In these circumstances the regular Build functions will not re-build those seeds. The Reconstruct Parcels From Seeds command is used for these cases; the API is shown here:
protected async override void OnClick()
{
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
if (myParcelFabricLayer == null)
{
MessageBox.Show("Please add a parcel fabric to the map.",
"Reconstruct from Seeds");
return;
}
string sReportResult = "";
string errorMessage = await QueuedTask.Run(async () =>
{
var editOper = new EditOperation()
{
Name = "Reconstruct from Seeds",
ProgressMessage = "Reconstruct from Seeds ...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = true,
SelectModifiedFeatures = false
};
try
{
List<FeatureLayer> parcelLayers = new();
List<string> sParcelTypes = new List<string> { "Tax", "Lot" };
foreach (string sParcTyp in sParcelTypes)
{
IEnumerable<FeatureLayer> lyrs =
await myParcelFabricLayer.GetParcelPolygonLayerByTypeNameAsync(sParcTyp);
parcelLayers.Add(lyrs.FirstOrDefault());
lyrs = await myParcelFabricLayer.GetParcelLineLayerByTypeNameAsync(sParcTyp);
parcelLayers.Add(lyrs.FirstOrDefault());
}
IEnumerable<FeatureLayer> ptLyrs = await myParcelFabricLayer.GetPointsLayerAsync();
parcelLayers.Add(ptLyrs.FirstOrDefault());
ParcelEditToken peToken = null;
peToken = editOper.ReconstructParcelsFromSeeds(myParcelFabricLayer,
MapView.Active.Extent.Expand(2.0,2.0,true), parcelLayers);
if (!editOper.Execute())
{
return editOper.ErrorMessage;
}
SelectionSet FeatSetCreated = null;
SelectionSet FeatSetModified = null;
if (peToken != null)
{
FeatSetCreated = peToken.CreatedFeatures;
FeatSetModified = peToken.ModifiedFeatures;
}
var editOperation2 = editOper.CreateChainedOperation();
sReportResult = "";
Dictionary<string, object> ParcelAttributes = new Dictionary<string, object>();
if (FeatSetModified != null)
{
foreach (var kvp in FeatSetModified.ToDictionary())
{
foreach (long oid in kvp.Value)
{//uncomment the next 3 lines to apply this example edit:
//ParcelAttributes.Add("Name", "My" + kvp.Key.Name + " " + oid.ToString());
//editOperation2.Modify(kvp.Key, oid, ParcelAttributes);
//ParcelAttributes.Clear();
}
sReportResult += "There were " + kvp.Value.Count().ToString() + " " + kvp.Key.Name +
" features modified." + Environment.NewLine;
}
}
if (FeatSetCreated != null)
{
foreach (var kvp in FeatSetCreated.ToDictionary())
{
foreach (long oid in kvp.Value)
{//uncomment the next 3 lines to apply this example edit:
//ParcelAttributes.Add("Name", "My" + kvp.Key.Name + " " + oid.ToString());
//editOperation2.Modify(kvp.Key, oid, ParcelAttributes);
//ParcelAttributes.Clear();
}
sReportResult += "There were " + kvp.Value.Count().ToString() + " new " + kvp.Key.Name +
" features created." + Environment.NewLine;
}
}
if (!editOperation2.IsEmpty)
{
if (!editOperation2.Execute())
return editOperation2.ErrorMessage;
}
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Reconstruct from Seeds");
else if (!string.IsNullOrEmpty(sReportResult))
MessageBox.Show(sReportResult, "Reconstruct from Seeds");
}
The standard delete method from the editing assembly is one valid approach for removing features from the parcel fabric. The DeleteParcels
method does additional work that is specific to the parcel information model. Using this method, the parcelsβ non-shared lines and points are also deleted.
await QueuedTask.Run( () =>
{
// check for selected layer
if (MapView.Active.GetSelectedLayers().Count == 0)
{
MessageBox.Show("Please select a source feature layer in the table of contents", "Delete Parcel");
return;
}
//first get the feature layer that's selected in the table of contents
var theParcelFeatLyr = MapView.Active.GetSelectedLayers().OfType<FeatureLayer>().First();
if (theParcelFeatLyr == null)
return;
var ids = new List<long>(theParcelFeatLyr.GetSelection().GetObjectIDs());
if (ids.Count == 0)
return;
try
{
var editOper = new EditOperation()
{
Name = "Delete Parcels",
ProgressMessage = "Delete Parcels...",
ShowModalMessageAfterFailure = true,
SelectNewFeatures = false,
SelectModifiedFeatures = false
};
editOper.DeleteParcels(theParcelFeatLyr, ids);
editOper.Execute();
}
catch
{
return;
}
});
When using the parcel API functions to create or update parcel features, it is often useful to be able to access those features for further action. The ParcelEditToken
can be used for this purpose. If this action includes edits of the features within the same edit operation then a chained edit operation must be used. For more information about chaining edit operations see the Editing API topic, Chaining Edit Operations.
Using BuildParcelsByRecord
as an example, there are two patterns for using these functions: the first without a parcel edit token, and the second with a token.
- No parcel edit token:
editOper.BuildParcelsByRecord(myParcelFabricLayer, guid);
- Using a parcel edit token requires the parcel feature layers
IEnumerable
on the (otherwise optional) third parameter:
var peToken = editOper.BuildParcelsByRecord(myParcelFabricLayer, guid, parcelLayers);
As shown in the first snippet above, if a parcel edit token is not required, then you donβt specify anything for the third parameter.
On the other hand, if you want to use a parcel edit token then the parcel layers that you pass in for the third parameter are the ones that you are interested in working with thereafter. If your code declares a return token but excludes the third parameter, then the code will compile and run, but at runtime you will recieve an empty ParcelEditToken
, with Count=0 for both modified and created features.
In the following code snippets the parcel edit token is used to do some additional work on the parcel features that are created or modified after executing BuildParcelByRecord
.
The token is acquired by declaring a return variable for the EditOperation
and it also requires that the IEnumerable
parcel feature layers be provided on the (otherwise optional) third parameter:
var MyParcelEditToken = editOper.BuildParcelsByRecord(myParcelFabricLayer, guid, parcelLayers)
The edit operation is then executed and a variable declared for the set of modified features and another is declared for the created features.
editOper.Execute();
var FeatSetCreated = MyParcelEditToken.CreatedFeatures;
var FeatSetModified = MyParcelEditToken.ModifiedFeatures;
The chained edit operation is created and the additional work is done as follows:
var editOperation2 = editOper.CreateChainedOperation();
string sReportResult="";
Dictionary<string, object> ParcelAttributes = new Dictionary<string, object>();
foreach (KeyValuePair<MapMember, List<long>> kvp in FeatSetModified.ToDictionary())
{
foreach (long oid in kvp.Value)
{
ParcelAttributes.Add("Name", "My" + kvp.Key.Name + " " + oid.ToString());
editOperation2.Modify(kvp.Key, oid, ParcelAttributes);
ParcelAttributes.Clear();
}
sReportResult += "There were " + kvp.Value.Count().ToString() + " " + kvp.Key.Name + " features modified." +
Environment.NewLine;
}
foreach (KeyValuePair<MapMember, List<long>> kvp in FeatSetCreated.ToDictionary())
{
foreach (long oid in kvp.Value)
{
//Do things with the created features
}
sReportResult += "There were " + kvp.Value.Count().ToString() + " new " + kvp.Key.Name + " features created." +
Environment.NewLine;
}
if ( editOperation2.Execute())
return sReportResult;
Note that not all of the Parcel API functions will return both Modified as well as Created features in the returned ParcelEditToken. The following table lists each of the functions, and if it is possible for its parcel edit token to carry modified features, created features, or both.
Parcel API Function Name | Supports created features? | Supports modified features? |
---|---|---|
AssignFeaturesToRecord | β | |
BuildParcelsByExtent | β | β |
BuildParcelsByRecord | β | β |
ChangeParcelType | β | β |
CopyParcelLinesToParcelType | β | |
CopyLineFeaturesToParcelType | β | |
CreateParcelSeedsByRecord | β | |
DeleteParcels | -- | -- |
DuplicateParcels | β | |
ReassignFeaturesToRecord | β | |
RetireFeaturesToRecord | β | |
SetParcelHistoryRetired | β | |
SetParcelHistoryCurrent | β | |
ShrinkParcelsToSeeds | β |
Since parcels have boundary lines around their perimeters and common shared parcel corners, it is useful to be able to use parcels' polygons to find the lines and points that form their geometry. An example of this can be found in the Records ribbon with the button called Get Parcel Features. Clicking this button will select parcel boundary lines and points based on the selected parcel polygons. A code equivalent of this button is demonstrated in the first section below. In addition to being able to get lines and points from a parcel polygon, you can also programmatically get the lines returned in a clockwise sequence from a given starting location. This is described in the section below called Get clockwise-sequenced parcel edge information.
The following code snippet is an example showing how to get back the lines and points from the selected polygons that belong to the parcel type called tax
.
protected async override void OnClick()
{
string sReportResult = "Polygon Information --" + Environment.NewLine;
string sParcelTypeName = "tax";
string errorMessage = await QueuedTask.Run(async () =>
{
var myParcelFabricLayer =
MapView.Active.Map.GetLayersAsFlattenedList().OfType<ParcelLayer>().FirstOrDefault();
//if there is no fabric in the map then bail
if (myParcelFabricLayer == null)
return "There is no fabric layer in the map.";
//first get the parcel type feature layer
var featSrcLyr = myParcelFabricLayer.GetParcelPolygonLayerByTypeNameAsync(sParcelTypeName).Result.FirstOrDefault();
if (featSrcLyr.SelectionCount == 0)
return "There is no selection on the " + sParcelTypeName + " layer.";
sReportResult += " Parcel Type: " + sParcelTypeName + Environment.NewLine;
sReportResult += " Poygons: " + featSrcLyr.SelectionCount + Environment.NewLine + Environment.NewLine;
try
{
// ------- get the selected parcels ---------
var ids = new List<long>((featSrcLyr as FeatureLayer).GetSelection().GetObjectIDs());
var sourceParcFeats = new Dictionary<MapMember, List<long>>();
sourceParcFeats.Add(featSrcLyr, ids);
//---------------------------------------------
ParcelFeatures parcFeatures =
await myParcelFabricLayer.GetParcelFeaturesAsync(SelectionSet.FromDictionary(sourceParcFeats));
//since we know that we want to report on Tax lines only, and for this functionality
// we can use any of the Tax line layer instances (if there happens to be more than one)
// we can get the first instance as follows
FeatureLayer myLineFeatureLyr =
myParcelFabricLayer.GetParcelLineLayerByTypeName(sParcelTypeName).Result.FirstOrDefault();
if (myLineFeatureLyr == null)
return sParcelTypeName + " line layer not found";
FeatureLayer myPointFeatureLyr =
myParcelFabricLayer.GetPointsLayerAsync().Result.FirstOrDefault();
if (myPointFeatureLyr == null)
return "fabric point layer not found";
var LineInfo = parcFeatures.Lines; //then get the line information from the parcel features object
//... and then do some work for each of the lines
int iRadiusAttributeCnt = 0;
int iDistanceAttributeCnt = 0;
sReportResult += "Line Information --";
foreach (KeyValuePair<string, List<long>> kvp in LineInfo)
{
if (kvp.Key.ToLower() != sParcelTypeName)
continue; // ignore any other lines from different parcel types
foreach (long oid in kvp.Value)
{
var insp = myLineFeatureLyr.Inspect(oid);
var dRadius = insp["RADIUS"];
var dDistance = insp["DISTANCE"];
if (dRadius != DBNull.Value)
iRadiusAttributeCnt++;
if (dDistance != DBNull.Value)
iDistanceAttributeCnt++;
//Polyline poly = (Polyline)insp["SHAPE"];
}
sReportResult += Environment.NewLine + " Distance attributes: " + iDistanceAttributeCnt.ToString();
sReportResult += Environment.NewLine + " Radius attributes: " + iRadiusAttributeCnt.ToString();
}
var PointInfo = parcFeatures.Points; //get the point information from the parcel features object
//... and then do some work for each of the points
sReportResult += Environment.NewLine + Environment.NewLine + "Point Information --";
int iFixedPointCnt = 0;
int iNonFixedPointCnt = 0;
foreach (long oid in PointInfo)
{
var insp = myPointFeatureLyr.Inspect(oid);
var isFixed = insp["ISFIXED"];
if (isFixed == DBNull.Value || (int)isFixed == 0)
iNonFixedPointCnt++;
else
iFixedPointCnt++;
// var pt = insp["SHAPE"];
}
sReportResult += Environment.NewLine + " Fixed Points: " + iFixedPointCnt.ToString();
sReportResult += Environment.NewLine + " Non-Fixed Points: " + iNonFixedPointCnt.ToString();
}
catch (Exception ex)
{
return ex.Message;
}
return "";
});
if (!string.IsNullOrEmpty(errorMessage))
MessageBox.Show(errorMessage, "Get Parcel Features");
else
MessageBox.Show(sReportResult, "Get Parcel Features");
}
The preceding section shows how to return a parcel's line and point features for use in simple customizations that do not require any specific sequence or order for the returned features. However, custom tools that need functionality like calculating a misclosure, or reporting a parcel's information in a line table or boundary layout need to order the lines into a connected sequence, often starting and ending at a known point of beginning. This section describes the Parcel API and the concepts used for the geometric relationships between a parcel's polygon geometry and its parcel boundary lines. The terms used or defined in this section include: parcel edges, geometry segments, parcel line-to-edge relationships, clockwise sequence, next line connectivity, previous line connectivity, and closing line.
Parcel edges are derived from the geometry of the polygon's segments and are established based on segment tangency. Each sequence of tangent segments defines a single parcel edge.
The illustration above shows a polygon geometry that has four edges. The east side of the polygon has two tangent segments that define one of the parcel edges. If this same polygon geometry were edited with additional vertices on the eastern side to result in multiple tangent segments there, then the parcel still has the same number of parcel edges, depicted as follows:
For circular arcs the segments that define a single parcel edge must be tangent and must also have the same radius. In the following illustration the polygon geometry has four circular arc segments, and they are all tangent to one another. However there are two parcel edges here because the northern-most segment has a different radius to the others.
The parcel edges returned via the function GetSequencedParcelEdgeInfoAsync
in the snippet below will always follow a clockwise sequence. By using a hint-point to specify the nearest point-of-beginning you can dictate the first parcel edge in the clockwise sequence.
Note also that the line features returned by the function will always be in the same parcel type as the parcel polygon layer passed in for the first parameter.
ParcelEdgeCollection myEdgeCollection = await
myParcelFabricLayer.GetSequencedParcelEdgeInfoAsync(MyPolygonLyr, myParceloid, pointOfBeginning);
If your implementation does not require a point of beginning; for example if you are only computing a parcel misclose, then the hint-point parameter can be set to null
and the sequence will be returned with an arbitrary start point.
The hint-point is a point thatβs close to your desired point of beginning and it could also be an exact match. When you use the hint-point the function will look for the closest lineβs start or end vertex, and this dictates the starting parcel edge for the clockwise sequence.
The parcel and boundary lines depicted in the following illustration are used to describe the relationships in this section:
The GetSequencedParcelEdgeInfoAsync
function uses the clockwise parcel edges and queries each one to get the relationships between the edge and the set of boundary line features. The line features that are returned are any that have full or partial overlap with the parcel edge.
For example the following image depicts the two lines 'A' and 'B' returned for the western parcel edge:
These lines are shown graphically separated for visualization purposes. They are exactly coincident, and otherwise differ only in that their geometry is reversed from one another by 180Β°.
There are two overall forms of these relationships, they are qualitative and quantitive. The qualitative realtionships are defined using four boolean flags and an enumeration. The enumeration below is used to describe a boundary line's start and end points in relation to the parcel edge that it overlaps.
public enum ParcelLineToEdgeRelationship
{
StartVertexOnAnEdge = 1,
EndVertexOnAnEdge = 2,
StartVertexMatchesAnEdgeEnd = 4,
EndVertexMatchesAnEdgeEnd = 8,
BothVerticesMatchAnEdgeEnd = 16,
NoEndVerticesTouchAnEdge = 32,
All = -1
}
The four boolean flags that define the other qualitative line properties are as follows:
-
IsReversed
: if the flag istrue
then the line direction is oriented in the opposite direction to the edge that it's on. -
IsClosing
: if the flag istrue
then the line is on the final parcel edge that connects back to the starting edge within the clockwise sequence. -
HasNextLineConnectivity
: if the flag istrue
then there is a line feature on the same parcel edge, or on the next parcel edge within the clockwise sequence, that is connected to the current line's end vertex. -
HasPreviousLineConnectivity
: if the flag istrue
then there is a line feature on the same parcel edge, or on the previous parcel edge within the clockwise sequence, that is connected to the current line's end vertex.
The function also returns quantitative properties based on the geometry of each parcel edge. It assigns a value of 0 at the parcel edges start and a value of 1 at its end. Then for each line feature that is on an edge, its start and end points' proportional positions are returned.
For example the following image depicts the 0
and 1
positions on the north parcel edge:
Introducing the three lines that are fully or partially coincident with this edge reveals that the line feature called 'A' has its start position at 0
, and its end position at 1
:
Since the other two lines in the image above partially overlap the parcel edge and extend beyond the 0
and 1
positions, they will have start and end positions that are greater than 1 or less than 0. A complete explanation of this is presented in the conceptual example topic below.
The following code generates a report with these line-to-edge relationships:
foreach (var edge in myEdgeCollection.Edges)
{
sReportResult += "Edge: " + edge.EdgeId.ToString() + " Geometry: ";
sReportResult += edge.EdgeGeometry == null ? "NULL\n\n" : edge.EdgeGeometry.Length.ToString("F2") + " Length\n\n";
foreach (var myLineInfo in edge.Lines)
{
sReportResult += "*Line-OID:" + myLineInfo.ObjectID;
sReportResult += "\nFromPnt:" + ListToString(myLineInfo.FromPointObjectID);
sReportResult += " ToPnt:" + ListToString(myLineInfo.ToPointObjectID);
sReportResult += "\nRecord:" + myLineInfo.RecordGUID.ToString();
sReportResult += "\nEdge IDs:" + ListToString(myLineInfo.EdgeId);
sReportResult += "\nRelation:" + myLineInfo.EdgeRelationship.ToString();
sReportResult += "\nReversed:" + myLineInfo.IsReversed;
sReportResult += " Closing:" + myLineInfo.IsClosing;
sReportResult += " NextCon:" + myLineInfo.HasNextLineConnectivity;
sReportResult += " PrevCon:" + myLineInfo.HasPreviousLineConnectivity;
sReportResult += "\nStart:" + myLineInfo.StartPositionOnParcelEdge.ToString("F2");
sReportResult += " End:" + myLineInfo.EndPositionOnParcelEdge.ToString("F2");
sReportResult += "\nAttributes:" + (myLineInfo.FeatureAttributes == null ? "NULL" : myLineInfo.FeatureAttributes.Count.ToString());
sReportResult += "\nGeometry:" + (myLineInfo.FeatureGeometry == null ? "NULL" : myLineInfo.FeatureGeometry.Length.ToString("F2") + " Length");
sReportResult += "\n\n";
}
}
private static string ListToString<T>(IReadOnlyList<T> ids)
{
string ret = "";
foreach (var id in ids)
{
if (ret.Count() > 0)
ret += ",";
ret += id;
}
return ret;
}
For natural boundaries and other types of line feature data, there may be multiple edges for a single line boundary as shown in this illustration:
In this scenario the EdgeID property in ParcelLineInfo
returns a list of ids, and this list is repeated for each parcel edge that is shared by the same line feature. The following shows a portion of the returned report for two consecutive edges for a line feature that has multiple parcel edges:
.
.
.
#Edge:2 Geometry: 100.19 Length
*Line-OID:16753
FromPnt:10548 ToPnt:10546
Edge IDs:2
Relation:BothVerticesMatchAnEdgeEnd
Reversed:False Closing:False NextCon:False PrevCon:True
Start:0.00 End:1.00
Attributes:29
Geometry:100.19 Length
#Edge:3 Geometry: 29.11 Length
*Line-OID:16751
FromPnt:10550 ToPnt:10551
Edge IDs:3,4,5,6,7,8,9,10,11,12,13,14
Relation:NoEndVerticesTouchAnEdge
Reversed:True Closing:False NextCon:False PrevCon:False
Start:8.59 End:-1.75
Attributes:18
Geometry:363.19 Length
#Edge:4 Geometry: 18.51 Length
*Line-OID:16751
FromPnt:10550 ToPnt:10551
Edge IDs:3,4,5,6,7,8,9,10,11,12,13,14
Relation:NoEndVerticesTouchAnEdge
Reversed:True Closing:False NextCon:False PrevCon:False
Start:13.81 End:-3.22
Attributes:18
Geometry:363.19 Length
.
.
.
From the information shown in this report it can be determined that the line oid:16751 has 11 parcel edges, and the first two edges have lengths of 29.11 and 18.51, respectively.
The GetSequencedParcelEdgeInfoAsync
function has an overload that allows you to specify the lines to return based on their properties. For example, if I am interested only in the lines that have both end vertices on a parcel edge, then I can use the following code, passing in the BothVerticesMatchAnEdgeEnd
value for the fourth parameter:
ParcelEdgeCollection parcelEdgeCollection = await
myParcelFabricLayer.GetSequencedParcelEdgeInfoAsync(myPolygonLayer, oid, null, ParcelLineToEdgeRelationship.BothVerticesMatchAnEdgeEnd);
The following code snippet shows how to access the geometry coordinates of the points after using the GetSequencedParcelEdgeInfoAsync
function:
ParcelEdgeCollection parcelEdgeCollection =
await myParcelFabricLayer.GetSequencedParcelEdgeInfoAsync(myPolygonLayer, oid, null);
//get the point layer
var pointLyrEnum = await myParcelFabricLayer.GetPointsLayerAsync();
if (pointLyrEnum.Count() == 0)
return "No point layer found.";
var myPointLyr = pointLyrEnum.FirstOrDefault();
foreach (var myPoint in parcelEdgeCollection.Points)
{
var id = (myPointLyr.Inspect(myPoint).ObjectID).ToString();
var x = (myPointLyr.Inspect(myPoint).Shape as MapPoint).X.ToString("F2");
var y = (myPointLyr.Inspect(myPoint).Shape as MapPoint).Y.ToString("F2");
sReportResult += id + ", X: " + x;
sReportResult += ", Y: " + y + "\n";
}
This section uses the same example as used earlier in this topic, with a detailed break-down of each of the line features' parcel edge relationships. The parcel and its lines are shown again here:
Line Feature | Parcel line-to-edge relationships | IsReversed ? | IsClosing? | HasNextLineConnectivity? | HasPreviousLineConnectivity? |
---|---|---|---|---|---|
A | BothVerticesMatchAnEdgeEnd | β | β | ||
B | BothVerticesMatchAnEdgeEnd | β | β | β |
Line Feature | StartPositionOnParcelEdge | EndPositionOnParcelEdge |
---|---|---|
A | 0.000000 | 1.000000 |
B | 1.000000 | 0.000000 |
Line Feature | Parcel line-to-edge relationships | IsReversed ? | IsClosing? | HasNextLineConnectivity? | HasPreviousLineConnectivity? |
---|---|---|---|---|---|
A | BothVerticesMatchAnEdgeEnd | β | β | ||
B | EndVertexMatchesAnEdgeEnd | β | β | ||
C | StartVertexMatchesAnEdgeEnd | β | β |
Line Feature | StartPositionOnParcelEdge | EndPositionOnParcelEdge |
---|---|---|
A | 0.000000 | 1.000000 |
B | 2.349436 | 0.000000 |
C | 1.000000 | -1.349436 |
Line Feature | Parcel line-to-edge relationships | IsReversed ? | IsClosing? | HasNextLineConnectivity? | HasPreviousLineConnectivity? |
---|---|---|---|---|---|
A | BothVerticesMatchAnEdgeEnd | β | β | ||
B | StartVertexMatchesAnEdgeEnd | β | β | ||
C | EndVertexMatchesAnEdgeEnd | β | β |
Line Feature | StartPositionOnParcelEdge | EndPositionOnParcelEdge |
---|---|---|
A | 0.000000 | 1.000000 |
B | 0.000000 | 0.499446 |
C | 0.500554 | 1.000000 |
Line Feature | Parcel line-to-edge relationships | IsReversed ? | IsClosing? | HasNextLineConnectivity? | HasPreviousLineConnectivity? |
---|---|---|---|---|---|
A | BothVerticesMatchAnEdgeEnd | β | β | β |
Line Feature | StartPositionOnParcelEdge | EndPositionOnParcelEdge |
---|---|---|
A | 0.000000 | 1.000000 |
When calculating the area and misclosure information for a parcel using the lines that form its boundary, you first need to determine if the lines of the parcel form a closed loop. Along with the line sequence, the direction and distance attributes on the lines are also needed. The following code and functions show how to determine these important characteristics using the concepts from the prior topics.
First get the parcel edge collection:
parcelEdgeCollection = await myParcelFabricLayer.GetSequencedParcelEdgeInfoAsync(MyParcelPolygonLayer,
MyParcelOID, null, offsetTolerance, ParcelLineToEdgeRelationship.All);
Then declare boolean variables for use in the function provided below called ParcelEdgeAnalysis
. These variables are used to report if the lines form a closed loop, and if they have COGO attributes:
object[] parcelTraverseInfo;
bool isClosedloop = false;
bool allLinesHaveCogo = false;
if (!ParcelEdgeAnalysis(parcelEdgeCollection, out isClosedloop, out allLinesHaveCogo,
out parcelTraverseInfo))
MessageBox.Show("No traverse available.");
if (isClosedloop && allLinesHaveCogo)
MessageBox.Show("Lines form a closed loop, and there is enough COGO information to calculate misclose.");
else if (isClosedloop && !allLinesHaveCogo)
MessageBox.Show("Lines form a closed loop, but there is not enough COGO information to calculate misclose.");
else if (!isClosedloop && allLinesHaveCogo)
MessageBox.Show("All lines found have COGO information, but they do not form a closed loop.");
else if (!isClosedloop && !allLinesHaveCogo)
MessageBox.Show("Lines do not form a closed loop, and one or more lines are missing COGO information.");
Here are the functions used in the previous code snippets:
internal bool ParcelEdgeAnalysis(ParcelEdgeCollection parcelEdgeCollection, out bool isClosedLoop,
out bool allLinesHaveCOGO, out object[] traverseInfo)
{
//traverseInfo object list items:
//1. vector, 2. direction, 3. distance,
//4. radius, 5. arclength, 6. isMajor, 7. isLineReversed
var vectorChord = new List<object>();
var directionList = new List<object>();
var distanceList = new List<object>();
var radiusList = new List<object>();
var arcLengthList = new List<object>();
var isMajorList = new List<bool>();
var isLineReversedList = new List<bool>();
try
{
isClosedLoop = true; //start optimistic
allLinesHaveCOGO = true; //start optimistic
foreach (var edge in parcelEdgeCollection.Edges)
{
var highestPosition = 0.0;
foreach (var myLineInfo in edge.Lines)
{
//test for COGO attributes and line type
bool hasCOGODirection = myLineInfo.FeatureAttributes.TryGetValue("Direction", out object direction);
bool hasCOGODistance = myLineInfo.FeatureAttributes.TryGetValue("Distance", out object distance);
bool hasCOGORadius = myLineInfo.FeatureAttributes.TryGetValue("Radius", out object radius);
bool hasCOGOArclength = myLineInfo.FeatureAttributes.TryGetValue("ArcLength", out object arclength);
bool bIsCOGOLine = hasCOGODirection && hasCOGODistance;
//logic to exclude unwanted lines on this edge
if (!myLineInfo.HasNextLineConnectivity)
continue;
if (myLineInfo.EndPositionOnParcelEdge > 1)
continue;
if (myLineInfo.EndPositionOnParcelEdge < 0)
continue;
if (myLineInfo.StartPositionOnParcelEdge > 1)
continue;
if (myLineInfo.StartPositionOnParcelEdge < 0)
continue;
//also exclude historic lines
bool hasRetiredByGuid = myLineInfo.FeatureAttributes.TryGetValue("RetiredByRecord", out object guid);
if (hasRetiredByGuid && guid != DBNull.Value)
continue;
directionList.Add(direction);
distanceList.Add(distance);
isLineReversedList.Add(myLineInfo.IsReversed);
if (!bIsCOGOLine)
{//circular arc
if (hasCOGODirection && hasCOGORadius && hasCOGOArclength) //circular arc
{
var dRadius = (double)radius;
var dArclength = (double)arclength;
double dCentralAngle = dArclength / dRadius;
var chordDistance = 2.0 * dRadius * Math.Sin(dCentralAngle / 2.0);
var flip = myLineInfo.IsReversed ? Math.PI : 0.0;
var radiansDirection = ((double)direction * Math.PI / 180.0) + flip;
Coordinate3D vect = new();
vect.SetPolarComponents(radiansDirection, 0.0, chordDistance);
if (ClockwiseDownStreamEdgePosition(myLineInfo) == highestPosition)
{//this line's start matches last line's end
vectorChord.Add(vect);
arcLengthList.Add(dArclength);
if (Math.Abs(dArclength / dRadius) > Math.PI)
isMajorList.Add(true);
else
isMajorList.Add(false);
if (myLineInfo.IsReversed)
radiusList.Add(-dRadius); //this is for properly calcluating area sector
else
radiusList.Add(dRadius);
}
}
else //not a cogo circular arc, nor a cogo line
{ //partial or no circular arc or line COGO
allLinesHaveCOGO = false;
vectorChord.Add(null);
radiusList.Add(null);
arcLengthList.Add(null);
isMajorList.Add(false);
}
}
else //this is a straight cogo line
{
var flip = myLineInfo.IsReversed ? Math.PI : 0.0;
var radiansDirection = ((double)direction * Math.PI / 180.0) + flip;
Coordinate3D vect = new();
vect.SetPolarComponents(radiansDirection, 0.0, (double)distance);
if (ClockwiseDownStreamEdgePosition(myLineInfo) == highestPosition)
{//this line's start matches previous line's end
vectorChord.Add(vect);
arcLengthList.Add(null);
radiusList.Add(null);
isMajorList.Add(false);
}
}
var UpstreamPos = ClockwiseUpStreamEdgePosition(myLineInfo);
highestPosition =
highestPosition > UpstreamPos ? highestPosition : UpstreamPos;
}
if (highestPosition != 1)
//we lost connectivity, not able to traverse all the way
//to the end of this edge
{
isClosedLoop = false; // no loop connectivity
break;
}
}
return true;
}
catch
{
isClosedLoop = false;
allLinesHaveCOGO = false;
return false;
}
finally
{
traverseInfo = new object[7] {vectorChord, directionList, distanceList,
radiusList, arcLengthList, isMajorList, isLineReversedList};
}
}
internal double ClockwiseDownStreamEdgePosition(ParcelLineInfo line)
{
return line.IsReversed ? line.EndPositionOnParcelEdge : line.StartPositionOnParcelEdge;
}
internal double ClockwiseUpStreamEdgePosition(ParcelLineInfo line)
{
return line.IsReversed ? line.StartPositionOnParcelEdge : line.EndPositionOnParcelEdge;
}