From 567fb521a96cecd76bd2be9a9a6f5774594fea32 Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 5 Aug 2026 11:02:44 -0600 Subject: [PATCH 01/14] chore(add-in): add post builds scripts to make deployments easier --- add-in/README.md | 12 ++++++++ .../Build/BumpAddInVersion.ps1 | 30 +++++++++++++++++++ add-in/UGRC.UtransTools/Config.daml | 4 +-- .../UGRC.UtransTools/UGRC.UtransTools.csproj | 18 +++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 add-in/UGRC.UtransTools/Build/BumpAddInVersion.ps1 diff --git a/add-in/README.md b/add-in/README.md index 580b5b3..2aae22e 100644 --- a/add-in/README.md +++ b/add-in/README.md @@ -37,3 +37,15 @@ This project uses **[CSharpier](https://csharpier.com/)** for opinionated, autom ```bash dotnet csharpier --check . ``` + +### Release Package + +Build the add-in in Release mode from the `add-in` directory: + +```powershell +dotnet build .\UGRC.UtransTools\UGRC.UtransTools.csproj --configuration Release +``` + +Or use Visual Studio to build the project in Release mode. + +The build copies `UGRC.UtransTools.esriAddinX` to `L:\agrc\utrans\UtransEditing\Pro Add-ins`. diff --git a/add-in/UGRC.UtransTools/Build/BumpAddInVersion.ps1 b/add-in/UGRC.UtransTools/Build/BumpAddInVersion.ps1 new file mode 100644 index 0000000..744d198 --- /dev/null +++ b/add-in/UGRC.UtransTools/Build/BumpAddInVersion.ps1 @@ -0,0 +1,30 @@ +param( + [Parameter(Mandatory = $true)] + [string]$ConfigPath +) + +$content = [System.IO.File]::ReadAllText($ConfigPath) +$pattern = '(]*\bversion\s*=\s*")(\d+)\.(\d+)(")' +$match = [System.Text.RegularExpressions.Regex]::Match($content, $pattern) + +if (-not $match.Success) { + throw "Could not find an AddInInfo version in '$ConfigPath'. Expected a major.minor version." +} + +$nextVersion = '{0}.{1}' -f $match.Groups[2].Value, ([int]$match.Groups[3].Value + 1) +$replaceVersion = [System.Text.RegularExpressions.MatchEvaluator]{ + param($versionMatch) + $versionMatch.Groups[1].Value + $nextVersion + $versionMatch.Groups[4].Value +} +$updatedContent = [System.Text.RegularExpressions.Regex]::Replace( + $content, + $pattern, + $replaceVersion, + 1) + +[System.IO.File]::WriteAllText( + $ConfigPath, + $updatedContent, + [System.Text.UTF8Encoding]::new($false)) + +Write-Host "Updated AddInInfo version to $nextVersion." diff --git a/add-in/UGRC.UtransTools/Config.daml b/add-in/UGRC.UtransTools/Config.daml index d1f378f..d6dc751 100644 --- a/add-in/UGRC.UtransTools/Config.daml +++ b/add-in/UGRC.UtransTools/Config.daml @@ -1,11 +1,11 @@ - + - + UGRC_UtransTools UGRC_UtransTools description Images\AddinDesktop32.png diff --git a/add-in/UGRC.UtransTools/UGRC.UtransTools.csproj b/add-in/UGRC.UtransTools/UGRC.UtransTools.csproj index 9fc2a68..21b65bd 100644 --- a/add-in/UGRC.UtransTools/UGRC.UtransTools.csproj +++ b/add-in/UGRC.UtransTools/UGRC.UtransTools.csproj @@ -104,6 +104,24 @@ Project="C:\Program Files\ArcGIS\Pro\bin\Esri.ProApp.SDK.Desktop.targets" Condition="Exists('C:\Program Files\ArcGIS\Pro\bin\Esri.ProApp.SDK.Desktop.targets') AND !Exists('Esri.ArcGISPro.Extensions.targets') AND '$(MSBuildRuntimeType)' == 'Full'" /> + + + + + + From e4a16c45cb2e3d81a86e6085ab7aca127daf9edb Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 5 Aug 2026 11:03:14 -0600 Subject: [PATCH 02/14] fix(add-in): better handling of broken layers --- .../Services/LayerValidationService.cs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/add-in/UGRC.UtransTools/Services/LayerValidationService.cs b/add-in/UGRC.UtransTools/Services/LayerValidationService.cs index 0686dcd..4b56a55 100644 --- a/add-in/UGRC.UtransTools/Services/LayerValidationService.cs +++ b/add-in/UGRC.UtransTools/Services/LayerValidationService.cs @@ -24,22 +24,33 @@ internal Task GetRequiredLayersAsync() ); var layers = map.GetLayersAsFlattenedList().OfType().ToList(); var missingAliases = new List(); + var invalidAliases = new List(); FeatureLayer GetLayer(string name) { var layer = layers.FirstOrDefault(candidate => string.Equals(candidate.Name, name, StringComparison.OrdinalIgnoreCase) - || string.Equals( - candidate.GetFeatureClass().GetDefinition().GetAliasName(), - name, - StringComparison.OrdinalIgnoreCase - ) ); if (layer is null) { missingAliases.Add(name); } + else + { + try + { + using var featureClass = layer.GetFeatureClass(); + if (featureClass is null) + { + invalidAliases.Add(name); + } + } + catch + { + invalidAliases.Add(name); + } + } return layer!; } @@ -59,6 +70,13 @@ FeatureLayer GetLayer(string name) ); } + if (invalidAliases.Count > 0) + { + throw new InvalidOperationException( + $"The active map has required layers with broken or unavailable data sources: {string.Join(", ", invalidAliases)}. Repair the layer data sources and try again." + ); + } + return new EditorLayerContext( utransRoads, countyRoads, From 7928f1e0e8d32adf8a1238b8b3ca7821fc86a2fe Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 19 Aug 2026 10:16:37 -0600 Subject: [PATCH 03/14] feat(add-in): set default SPEED_LMT = "25" --- add-in/UGRC.UtransTools/Models/EditorReviewState.cs | 4 +++- add-in/UGRC.UtransTools/Services/LayerValidationService.cs | 2 +- add-in/UGRC.UtransTools/Services/UtransEditService.cs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/add-in/UGRC.UtransTools/Models/EditorReviewState.cs b/add-in/UGRC.UtransTools/Models/EditorReviewState.cs index 044e1c9..6c15541 100644 --- a/add-in/UGRC.UtransTools/Models/EditorReviewState.cs +++ b/add-in/UGRC.UtransTools/Models/EditorReviewState.cs @@ -58,7 +58,9 @@ internal EditorReviewState(DfcSelectionSnapshot selection) VerticalLevel = string.IsNullOrWhiteSpace(utransRoad?.GetText("VERT_LEVEL")) ? "0" : utransRoad!.GetText("VERT_LEVEL"); - SpeedLimit = utransRoad?.GetText("SPEED_LMT") ?? string.Empty; + SpeedLimit = string.IsNullOrWhiteSpace(utransRoad?.GetText("SPEED_LMT")) + ? "25" + : utransRoad!.GetText("SPEED_LMT"); _initialCartocode = Cartocode; _initialOneway = Oneway; _initialVerticalLevel = VerticalLevel; diff --git a/add-in/UGRC.UtransTools/Services/LayerValidationService.cs b/add-in/UGRC.UtransTools/Services/LayerValidationService.cs index 4b56a55..5466616 100644 --- a/add-in/UGRC.UtransTools/Services/LayerValidationService.cs +++ b/add-in/UGRC.UtransTools/Services/LayerValidationService.cs @@ -100,7 +100,7 @@ internal Task< StringComparer.OrdinalIgnoreCase ); - foreach (var fieldName in new[] { "CARTOCODE", "ONEWAY", "VERT_LEVEL" }) + foreach (var fieldName in new[] { "CARTOCODE", "ONEWAY", "VERT_LEVEL", "STATUS" }) { var field = fields.FirstOrDefault(candidate => diff --git a/add-in/UGRC.UtransTools/Services/UtransEditService.cs b/add-in/UGRC.UtransTools/Services/UtransEditService.cs index 868c7b9..7342e5b 100644 --- a/add-in/UGRC.UtransTools/Services/UtransEditService.cs +++ b/add-in/UGRC.UtransTools/Services/UtransEditService.cs @@ -144,7 +144,7 @@ internal Task SaveAsync(EditorLayerContext layers, EditorReviewState state) ["CARTOCODE"] = state.Cartocode, ["ONEWAY"] = state.Oneway, ["VERT_LEVEL"] = state.VerticalLevel, - ["SPEED_LMT"] = string.IsNullOrWhiteSpace(state.SpeedLimit) ? null : state.SpeedLimit, + ["SPEED_LMT"] = state.SpeedLimit, ["FULLNAME"] = BuildFullName(values), }; From 7d643c52f22f4385c905facec1957b71210db44f Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 19 Aug 2026 10:17:23 -0600 Subject: [PATCH 04/14] feat(add-in): add "STATUS" field with default as "Active" --- .../UtransEditorConfiguration.cs | 1 + .../Models/EditorReviewState.cs | 19 ++++++++++ .../Services/UtransEditService.cs | 2 ++ .../UtransEditorDockpaneView.xaml | 35 ++++++++++--------- .../UtransEditorDockpaneViewModel.cs | 3 ++ 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/add-in/UGRC.UtransTools/Configuration/UtransEditorConfiguration.cs b/add-in/UGRC.UtransTools/Configuration/UtransEditorConfiguration.cs index 66022b6..eb6ccee 100644 --- a/add-in/UGRC.UtransTools/Configuration/UtransEditorConfiguration.cs +++ b/add-in/UGRC.UtransTools/Configuration/UtransEditorConfiguration.cs @@ -67,6 +67,7 @@ internal static class UtransEditorConfiguration "ONEWAY", "VERT_LEVEL", "SPEED_LMT", + "STATUS", }; internal static readonly IReadOnlyDictionary CountyOwnershipNames = diff --git a/add-in/UGRC.UtransTools/Models/EditorReviewState.cs b/add-in/UGRC.UtransTools/Models/EditorReviewState.cs index 6c15541..972c254 100644 --- a/add-in/UGRC.UtransTools/Models/EditorReviewState.cs +++ b/add-in/UGRC.UtransTools/Models/EditorReviewState.cs @@ -18,6 +18,7 @@ internal sealed class EditorReviewState : INotifyPropertyChanged private string _oneway = string.Empty; private string _verticalLevel = string.Empty; private string _speedLimit = string.Empty; + private string _status = string.Empty; internal EditorReviewState() { @@ -61,6 +62,9 @@ internal EditorReviewState(DfcSelectionSnapshot selection) SpeedLimit = string.IsNullOrWhiteSpace(utransRoad?.GetText("SPEED_LMT")) ? "25" : utransRoad!.GetText("SPEED_LMT"); + Status = string.IsNullOrWhiteSpace(utransRoad?.GetText("STATUS")) + ? "Active" + : utransRoad!.GetText("STATUS"); _initialCartocode = Cartocode; _initialOneway = Oneway; _initialVerticalLevel = VerticalLevel; @@ -85,6 +89,21 @@ AttributeReviewField CreateField(string fieldName, bool isAddressRange) => ); } + public string Status + { + get => _status; + set + { + if (_status == value) + { + return; + } + + _status = value; + OnPropertyChanged(); + } + } + internal DfcSelectionSnapshot? Selection { get; } public event PropertyChangedEventHandler? PropertyChanged; public string ChangeLabel => Selection?.ChangeLabel ?? string.Empty; diff --git a/add-in/UGRC.UtransTools/Services/UtransEditService.cs b/add-in/UGRC.UtransTools/Services/UtransEditService.cs index 7342e5b..8717ca6 100644 --- a/add-in/UGRC.UtransTools/Services/UtransEditService.cs +++ b/add-in/UGRC.UtransTools/Services/UtransEditService.cs @@ -148,6 +148,8 @@ internal Task SaveAsync(EditorLayerContext layers, EditorReviewState state) ["FULLNAME"] = BuildFullName(values), }; + values["STATUS"] = state.Status; + return values; } diff --git a/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml b/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml index 42ca878..0ee812c 100644 --- a/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml +++ b/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml @@ -444,7 +444,7 @@ - + @@ -513,16 +513,9 @@ From 2388d40eba3423a185f4af74a8bbddbd423a1687 Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 19 Aug 2026 10:39:49 -0600 Subject: [PATCH 06/14] fix(add-in): change cell color to white after update to match Fixes #13 --- add-in/UGRC.UtransTools/Models/AttributeReviewField.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/add-in/UGRC.UtransTools/Models/AttributeReviewField.cs b/add-in/UGRC.UtransTools/Models/AttributeReviewField.cs index a590838..be8427e 100644 --- a/add-in/UGRC.UtransTools/Models/AttributeReviewField.cs +++ b/add-in/UGRC.UtransTools/Models/AttributeReviewField.cs @@ -28,7 +28,7 @@ bool isAddressRange public string OriginalUtransValue { get; } public bool IsAddressRange { get; } public bool IsDifferent => - !string.Equals(CountyValue, OriginalUtransValue, System.StringComparison.OrdinalIgnoreCase); + !string.Equals(CountyValue, UtransValue, System.StringComparison.OrdinalIgnoreCase); public bool IsEdited => !string.Equals(UtransValue, OriginalUtransValue, System.StringComparison.Ordinal); public bool IsUsingCountyValue => _isUsingCountyValue; @@ -50,6 +50,7 @@ public string UtransValue _utransValue = value; OnPropertyChanged(); + OnPropertyChanged(nameof(IsDifferent)); OnPropertyChanged(nameof(IsEdited)); } } From 3e3fa41df78c162b08b1c617e056de4485321863 Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 19 Aug 2026 10:43:20 -0600 Subject: [PATCH 07/14] fix(add-in): clear form on save Ref #13 --- add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs b/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs index aad2732..77fc076 100644 --- a/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs +++ b/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs @@ -205,7 +205,10 @@ private async Task SaveAsync() { var layers = await _layerValidationService.GetRequiredLayersAsync(); await _utransEditService.SaveAsync(layers, ReviewState); - StatusMessage = $"Saved DFC record {ReviewState.Selection.ObjectId}."; + var savedObjectId = ReviewState.Selection.ObjectId; + ReviewState = null; + ChangeTypeMessage = "Please select one feature from DFC_RESULT layer."; + StatusMessage = $"Saved DFC record {savedObjectId}."; } catch (Exception exception) { From 1dfe222f0a61866b229fc263416ca24e82249198 Mon Sep 17 00:00:00 2001 From: Scott Davis Date: Wed, 19 Aug 2026 12:45:19 -0600 Subject: [PATCH 08/14] feat(add-in): implement "Add New" button Ref #12 --- .../Services/UtransEditService.cs | 110 ++++++++++++++---- .../UtransEditorDockpaneView.xaml | 9 +- .../UtransEditorDockpaneViewModel.cs | 28 ++++- 3 files changed, 120 insertions(+), 27 deletions(-) diff --git a/add-in/UGRC.UtransTools/Services/UtransEditService.cs b/add-in/UGRC.UtransTools/Services/UtransEditService.cs index 8717ca6..469c929 100644 --- a/add-in/UGRC.UtransTools/Services/UtransEditService.cs +++ b/add-in/UGRC.UtransTools/Services/UtransEditService.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using ArcGIS.Core.Data; using ArcGIS.Desktop.Editing; using ArcGIS.Desktop.Framework.Threading.Tasks; +using ArcGIS.Desktop.Mapping; using UGRC.UtransTools.Configuration; using UGRC.UtransTools.Models; @@ -11,6 +13,71 @@ namespace UGRC.UtransTools.Services; internal sealed class UtransEditService { + internal Task CreateNewUtransRoadAsync(EditorLayerContext layers, EditorReviewState state) + { + return QueuedTask.Run(async () => + { + if (state.Selection is not { IsNotYetCopiedNewRecord: true } selection) + { + throw new InvalidOperationException( + "The selected DFC record is not eligible to create a new UTRANS road." + ); + } + + var createOperation = new EditOperation + { + Name = "Add new UTRANS road", + SelectModifiedFeatures = false, + }; + var newRoad = createOperation.Create( + layers.UtransRoads, + selection.CountyRoad.Shape, + GetCountyRoadValues(selection.CountyRoad) + ); + if (!await createOperation.ExecuteAsync()) + { + throw new InvalidOperationException( + createOperation.ErrorMessage ?? "The new UTRANS road could not be created." + ); + } + + var linkOperation = new EditOperation + { + Name = "Link DFC record to new UTRANS road", + SelectModifiedFeatures = false, + }; + linkOperation.Modify( + layers.DfcResults, + selection.ObjectId, + new Dictionary { ["BASE_FID"] = newRoad.ObjectID } + ); + if (!await linkOperation.ExecuteAsync()) + { + throw new InvalidOperationException( + linkOperation.ErrorMessage ?? "The new UTRANS road could not be linked to DFC." + ); + } + + var dfcObjectIdField = layers + .DfcResults.GetFeatureClass() + .GetDefinition() + .GetObjectIDField(); + layers.DfcResults.Select( + new QueryFilter { WhereClause = $"{dfcObjectIdField} = {selection.ObjectId}" }, + SelectionCombinationMethod.New + ); + + var utransObjectIdField = layers + .UtransRoads.GetFeatureClass() + .GetDefinition() + .GetObjectIDField(); + layers.UtransRoads.Select( + new QueryFilter { WhereClause = $"{utransObjectIdField} = {newRoad.ObjectID}" }, + SelectionCombinationMethod.New + ); + }); + } + internal Task RepairDfcIdentifierAsync(EditorLayerContext layers, EditorReviewState state) { return QueuedTask.Run(async () => @@ -71,22 +138,12 @@ internal Task SaveAsync(EditorLayerContext layers, EditorReviewState state) if (shouldWriteRoad && state.Selection.UtransRoad is null) { - var newObjectId = operation.Create( - layers.UtransRoads, - state.Selection.CountyRoad.Shape, - values - ); - operation.Modify( - layers.DfcResults, - state.Selection.ObjectId, - new Dictionary - { - ["BASE_FID"] = newObjectId, - [UtransEditorConfiguration.DfcDispositionField] = state.DfcStatus, - } + throw new InvalidOperationException( + "Click Add New to create the target UTRANS road before saving." ); } - else if (shouldWriteRoad) + + if (shouldWriteRoad) { operation.Modify(layers.UtransRoads, state.Selection.BaseFeatureId, values); operation.Modify( @@ -123,17 +180,6 @@ internal Task SaveAsync(EditorLayerContext layers, EditorReviewState state) { var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (state.Selection.UtransRoad is null) - { - foreach (var fieldName in UtransEditorConfiguration.CommonRoadFields) - { - if (state.Selection.CountyRoad.Attributes.TryGetValue(fieldName, out var value)) - { - values[fieldName] = value; - } - } - } - foreach (var pair in state.GetEditedValues()) { values[pair.Key] = pair.Value; @@ -153,6 +199,20 @@ internal Task SaveAsync(EditorLayerContext layers, EditorReviewState state) return values; } + private static Dictionary GetCountyRoadValues(RoadSnapshot countyRoad) + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var fieldName in UtransEditorConfiguration.CommonRoadFields) + { + if (countyRoad.Attributes.TryGetValue(fieldName, out var value)) + { + values[fieldName] = value; + } + } + + return values; + } + private static string BuildFullName(IReadOnlyDictionary values) { var name = GetText(values, "NAME"); diff --git a/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml b/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml index 4fede80..28f470a 100644 --- a/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml +++ b/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml @@ -511,12 +511,19 @@ Margin="3" /> +