diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5c21a1d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "csharpier.csharpier-vscode" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b469984..aea395f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "Geospatial", "HWYNAM", "MVVM", + "ONEWAY", "pathspec", "POSTDIR", "POSTTYPE", @@ -36,5 +37,21 @@ ], "python.testing.pytestArgs": ["cli"], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "chat.tools.terminal.autoApprove": { + "/^dotnet build add-in\\\\UGRC\\.UtransTools\\\\UGRC\\.UtransTools\\.csproj --no-restore$/": { + "approve": true, + "matchCommandLine": true + } + }, + "[csharp]": { + "editor.defaultFormatter": "csharpier.csharpier-vscode" + }, + "[xml]": { + "editor.defaultFormatter": "csharpier.csharpier-vscode" + }, + "[xaml]": { + "editor.formatOnSave": false + }, + "editor.formatOnSave": true } diff --git a/add-in/README.md b/add-in/README.md index 580b5b3..53ce0b8 100644 --- a/add-in/README.md +++ b/add-in/README.md @@ -1,4 +1,18 @@ -# UTRANS Tools ArcGIS Pro Add-In +# UTrans Tools ArcGIS Pro Add-In + +## Usage + +### Adding New Features + +Use **Add New** to copy an unlinked new record or records from `DFC_RESULT` into the UTrans roads layer. + +1. In the map, select one or more unlinked records in the `DFC_RESULT` layer. A record is eligible when its change type is `N` and its `BASE_FID` is `-1`. +2. Review or update the road attributes shown in the editor. When multiple records are selected, the values are used for each new road. +3. Click **Add New**. + +When multiple features are selected, the form values override the county-road values for `CARTOCODE`, `ONEWAY`, `VERT_LEVEL`, `SPEED_LMT`, and `STATUS`. Review these values before clicking **Add New** because the same editor values are applied to every new road. + +The add-in creates a UTrans road using each selected county-road geometry and attributes, links the `DFC_RESULT` record to the new road, and marks the DFC disposition as `COMPLETED`. The map selections are cleared after the operation finishes. ## Developer Set Up @@ -29,11 +43,23 @@ This project uses **[CSharpier](https://csharpier.com/)** for opinionated, autom - **Format all files manually:** ```bash - dotnet csharpier . + dotnet csharpier format . ``` - **Verify formatting (used in CI):** ```bash - dotnet csharpier --check . + 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..03eea27 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/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/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)); } } diff --git a/add-in/UGRC.UtransTools/Models/EditorReviewState.cs b/add-in/UGRC.UtransTools/Models/EditorReviewState.cs index 044e1c9..a4f93c2 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() { @@ -30,6 +31,16 @@ internal EditorReviewState() ) ) ); + Cartocode = "11"; + Oneway = "0"; + VerticalLevel = "0"; + SpeedLimit = "25"; + Status = "Active"; + DfcStatus = "COMPLETED"; + _initialCartocode = Cartocode; + _initialOneway = Oneway; + _initialVerticalLevel = VerticalLevel; + _initialSpeedLimit = SpeedLimit; static AttributeReviewField CreateEmptyField(string fieldName, bool isAddressRange) => new(fieldName, string.Empty, string.Empty, isAddressRange); @@ -39,6 +50,7 @@ internal EditorReviewState(DfcSelectionSnapshot selection) { Selection = selection; var utransRoad = selection.UtransRoad; + var roadValues = utransRoad ?? selection.CountyRoad; Fields = new ObservableCollection( UtransEditorConfiguration .EditableAddressFields.Select(field => CreateField(field, true)) @@ -49,16 +61,21 @@ internal EditorReviewState(DfcSelectionSnapshot selection) ) ); - Cartocode = string.IsNullOrWhiteSpace(utransRoad?.GetText("CARTOCODE")) + Cartocode = string.IsNullOrWhiteSpace(roadValues.GetText("CARTOCODE")) ? "11" - : utransRoad!.GetText("CARTOCODE"); - Oneway = string.IsNullOrWhiteSpace(utransRoad?.GetText("ONEWAY")) + : roadValues.GetText("CARTOCODE"); + Oneway = string.IsNullOrWhiteSpace(roadValues.GetText("ONEWAY")) ? "0" - : utransRoad!.GetText("ONEWAY"); - VerticalLevel = string.IsNullOrWhiteSpace(utransRoad?.GetText("VERT_LEVEL")) + : roadValues.GetText("ONEWAY"); + VerticalLevel = string.IsNullOrWhiteSpace(roadValues.GetText("VERT_LEVEL")) ? "0" - : utransRoad!.GetText("VERT_LEVEL"); - SpeedLimit = utransRoad?.GetText("SPEED_LMT") ?? string.Empty; + : roadValues.GetText("VERT_LEVEL"); + SpeedLimit = string.IsNullOrWhiteSpace(roadValues.GetText("SPEED_LMT")) + ? "25" + : roadValues.GetText("SPEED_LMT"); + Status = string.IsNullOrWhiteSpace(roadValues.GetText("STATUS")) + ? "Active" + : roadValues.GetText("STATUS"); _initialCartocode = Cartocode; _initialOneway = Oneway; _initialVerticalLevel = VerticalLevel; @@ -83,6 +100,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/DfcSelectionService.cs b/add-in/UGRC.UtransTools/Services/DfcSelectionService.cs index b7311c9..26f5230 100644 --- a/add-in/UGRC.UtransTools/Services/DfcSelectionService.cs +++ b/add-in/UGRC.UtransTools/Services/DfcSelectionService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Threading.Tasks; using ArcGIS.Core.Data; using ArcGIS.Desktop.Framework.Threading.Tasks; @@ -64,6 +65,54 @@ internal Task GetRemainingCountAsync(EditorLayerContext layers) }); } + internal Task> LoadSelectedNewRecordsAsync( + EditorLayerContext layers + ) + { + return QueuedTask.Run(() => + { + var selectedObjectIds = layers.DfcResults.GetSelection().GetObjectIDs(); + if (selectedObjectIds.Count < 2) + { + return (IReadOnlyList)[]; + } + + using var dfcFeatureClass = layers.DfcResults.GetFeatureClass(); + using var countyRoadsFeatureClass = layers.CountyRoads.GetFeatureClass(); + var selections = selectedObjectIds + .Select(objectId => + { + var dfc = ReadFeature(dfcFeatureClass, objectId); + var updateFeatureId = ReadInt64(dfc.Attributes, "UPDATE_FID"); + var baseFeatureId = ReadInt64(dfc.Attributes, "BASE_FID"); + var changeType = dfc.GetText("CHANGE_TYPE"); + return new DfcSelectionSnapshot( + dfc.ObjectId, + updateFeatureId, + baseFeatureId, + changeType, + GetChangeLabel(changeType, baseFeatureId), + dfc, + ReadFeature(countyRoadsFeatureClass, updateFeatureId), + null + ); + }) + .ToList(); + + var ineligibleObjectIds = selections + .Where(selection => !selection.IsNotYetCopiedNewRecord) + .Select(selection => selection.ObjectId); + if (ineligibleObjectIds.Any()) + { + throw new InvalidOperationException( + $"Only unlinked New DFC_RESULT features can be copied. Ineligible ObjectIDs: {string.Join(", ", ineligibleObjectIds)}." + ); + } + + return selections; + }); + } + private static RoadSnapshot ReadFeature(FeatureClass featureClass, long objectId) { var objectIdField = featureClass.GetDefinition().GetObjectIDField(); diff --git a/add-in/UGRC.UtransTools/Services/LayerValidationService.cs b/add-in/UGRC.UtransTools/Services/LayerValidationService.cs index 0686dcd..5466616 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, @@ -82,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..6006eb2 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,80 @@ namespace UGRC.UtransTools.Services; internal sealed class UtransEditService { + internal Task CreateNewUtransRoadsAsync( + EditorLayerContext layers, + IReadOnlyList selections, + EditorReviewState roadValues + ) + { + return QueuedTask.Run(async () => + { + if ( + selections.Count == 0 + || selections.Any(selection => !selection.IsNotYetCopiedNewRecord) + ) + { + throw new InvalidOperationException( + "All selected DFC records must be unlinked New records to create UTRANS roads." + ); + } + + var createOperation = new EditOperation + { + Name = "Add new UTRANS road", + SelectModifiedFeatures = false, + }; + using var utransFeatureClass = layers.UtransRoads.GetFeatureClass(); + var newRoads = selections + .Select(selection => new + { + Selection = selection, + NewRoad = createOperation.Create( + layers.UtransRoads, + selection.CountyRoad.Shape, + GetCountyRoadValues( + selection.CountyRoad, + utransFeatureClass, + roadValues, + selections.Count > 1 + ) + ), + }) + .ToList(); + if (!await createOperation.ExecuteAsync()) + { + throw new InvalidOperationException( + createOperation.ErrorMessage ?? "The new UTRANS roads could not be created." + ); + } + + var linkOperation = new EditOperation + { + Name = "Link DFC record to new UTRANS road", + SelectModifiedFeatures = false, + }; + foreach (var newRoad in newRoads) + { + var values = new Dictionary + { + ["BASE_FID"] = newRoad.NewRoad.ObjectID, + [UtransEditorConfiguration.DfcDispositionField] = "COMPLETED", + }; + + linkOperation.Modify(layers.DfcResults, newRoad.Selection.ObjectId, values); + } + if (!await linkOperation.ExecuteAsync()) + { + throw new InvalidOperationException( + linkOperation.ErrorMessage ?? "The new UTRANS roads could not be linked to DFC." + ); + } + + layers.DfcResults.ClearSelection(); + layers.UtransRoads.ClearSelection(); + }); + } + internal Task RepairDfcIdentifierAsync(EditorLayerContext layers, EditorReviewState state) { return QueuedTask.Run(async () => @@ -71,22 +147,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 +189,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; @@ -144,10 +199,40 @@ 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), }; + values["STATUS"] = state.Status; + + return values; + } + + private static Dictionary GetCountyRoadValues( + RoadSnapshot countyRoad, + FeatureClass utransFeatureClass, + EditorReviewState roadValues, + bool applyRoadValueOverrides + ) + { + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var field in utransFeatureClass.GetDefinition().GetFields()) + { + if (field.IsEditable && countyRoad.Attributes.TryGetValue(field.Name, out var value)) + { + values[field.Name] = value; + } + } + + if (applyRoadValueOverrides) + { + values["CARTOCODE"] = roadValues.Cartocode; + values["ONEWAY"] = roadValues.Oneway; + values["VERT_LEVEL"] = roadValues.VerticalLevel; + values["SPEED_LMT"] = roadValues.SpeedLimit; + values["STATUS"] = roadValues.Status; + } + return values; } 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'" /> + + + + + + diff --git a/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml b/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml index 42ca878..323bea9 100644 --- a/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml +++ b/add-in/UGRC.UtransTools/UtransEditorDockpaneView.xaml @@ -72,6 +72,12 @@ Foreground="DarkGoldenrod" FontWeight="Bold" /> + - + - + - + diff --git a/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs b/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs index 427cd98..b583c39 100644 --- a/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs +++ b/add-in/UGRC.UtransTools/UtransEditorDockpaneViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; @@ -18,6 +19,8 @@ internal sealed class UtransEditorDockpaneViewModel : DockPane, INotifyPropertyC { private const string DockPaneId = "UGRC_UtransTools_UtransEditorDockpane"; private const string DefaultVersionMessage = "not versioned"; + private const string SelectDfcFeatureMessage = + "Please select one or more features from the DFC_RESULT layer."; private static readonly EditorReviewState EmptyReviewState = new(); private readonly LayerValidationService _layerValidationService = new(); private readonly DfcSelectionService _dfcSelectionService = new(); @@ -25,14 +28,18 @@ internal sealed class UtransEditorDockpaneViewModel : DockPane, INotifyPropertyC private readonly UtransEditService _utransEditService = new(); private EditorReviewState? _reviewState; private int? _remainingDfcRecords; - private string _changeTypeMessage = "Please select a feature from DFC_RESULT layer"; + private string _changeTypeMessage = SelectDfcFeatureMessage; private string _statusMessage = "Select one DFC_RESULT feature to load it in the editor"; private string _updateDfcObjectIdErrorMessage = string.Empty; private string _utransDatabaseVersion = DefaultVersionMessage; + private IReadOnlyList _selectedNewRecords = []; + private EditorReviewState? _newRoadValueState; + private string _selectedDfcObjectIds = string.Empty; private bool _codedValueOptionsLoaded; internal UtransEditorDockpaneViewModel() { + AddNewCommand = new AsyncRelayCommand(AddNewAsync); SaveCommand = new AsyncRelayCommand(SaveAsync); NextCommand = new AsyncRelayCommand(SelectNextDfcAsync); UpdateDfcObjectIdCommand = new AsyncRelayCommand(RepairDfcIdentifierAsync); @@ -93,12 +100,28 @@ private set OnPropertyChanged(); OnPropertyChanged(nameof(HasReviewState)); OnPropertyChanged(nameof(AvailableReviewState)); + OnPropertyChanged(nameof(CanAddNew)); + OnPropertyChanged(nameof(CanEditRoadValues)); + OnPropertyChanged(nameof(RoadValueState)); + OnPropertyChanged(nameof(DisplayDfcStatus)); } } - public bool HasReviewState => ReviewState is not null; + public bool HasReviewState => ReviewState?.Selection?.UtransRoad is not null; + + public bool CanAddNew => + _selectedNewRecords.Count > 0 || ReviewState?.Selection?.IsNotYetCopiedNewRecord == true; + + public bool CanEditRoadValues => HasReviewState || CanAddNew; + + public bool HasMultipleNewRecords => _selectedNewRecords.Count > 1; + + public string DisplayDfcStatus => + HasMultipleNewRecords ? "COMPLETED" : ReviewState?.DfcStatus ?? string.Empty; public EditorReviewState AvailableReviewState => ReviewState ?? EmptyReviewState; + public EditorReviewState RoadValueState => + _newRoadValueState ?? ReviewState ?? EmptyReviewState; public string ChangeTypeMessage { @@ -115,6 +138,21 @@ private set } } + public string SelectedDfcObjectIds + { + get => _selectedDfcObjectIds; + private set + { + if (_selectedDfcObjectIds == value) + { + return; + } + + _selectedDfcObjectIds = value; + OnPropertyChanged(); + } + } + public int? RemainingDfcRecords { get => _remainingDfcRecords; @@ -175,6 +213,7 @@ private set } } + public ICommand AddNewCommand { get; } public ICommand SaveCommand { get; } public ICommand NextCommand { get; } public ICommand UpdateDfcObjectIdCommand { get; } @@ -184,6 +223,7 @@ private set public IReadOnlyList CartocodeValues { get; private set; } = []; public IReadOnlyList OnewayValues { get; private set; } = []; public IReadOnlyList VerticalLevelValues { get; private set; } = []; + public IReadOnlyList StatusValues { get; private set; } = []; public IReadOnlyList SpeedLimitValues { get; } = CreateRange(5, 80, 5); public IReadOnlyList DfcStatusValues { get; } = new[] { "COMPLETED", "IGNORE", "REVISIT" }; @@ -193,6 +233,35 @@ internal static void Show() FrameworkApplication.DockPaneManager.Find(DockPaneId)?.Activate(); } + private async Task AddNewAsync() + { + if (_selectedNewRecords.Count == 0 && ReviewState is null) + { + return; + } + + try + { + var layers = await _layerValidationService.GetRequiredLayersAsync(); + var selections = + _selectedNewRecords.Count > 0 ? _selectedNewRecords + : ReviewState?.Selection is { IsNotYetCopiedNewRecord: true } selection + ? [selection] + : []; + await _utransEditService.CreateNewUtransRoadsAsync(layers, selections, RoadValueState); + ReviewState = null; + SetSelectedNewRecords([]); + RemainingDfcRecords = await _dfcSelectionService.GetRemainingCountAsync(layers); + ChangeTypeMessage = SelectDfcFeatureMessage; + StatusMessage = + $"Created {selections.Count} UTRANS road(s) for DFC record(s) {string.Join(", ", selections.Select(selection => selection.ObjectId))}."; + } + catch (Exception exception) + { + StatusMessage = exception.Message; + } + } + private async Task SaveAsync() { if (ReviewState is null) @@ -204,7 +273,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 = SelectDfcFeatureMessage; + StatusMessage = $"Saved DFC record {savedObjectId}."; } catch (Exception exception) { @@ -277,11 +349,24 @@ private async Task LoadSelectedDfcAsync() RemainingDfcRecords = await _dfcSelectionService.GetRemainingCountAsync(layers); UtransDatabaseVersion = await versionTask; + var selectedNewRecords = await _dfcSelectionService.LoadSelectedNewRecordsAsync(layers); + if (selectedNewRecords.Count > 0) + { + ReviewState = null; + SetSelectedNewRecords(selectedNewRecords); + ChangeTypeMessage = "New"; + StatusMessage = + $"Selected new DFC records {SelectedDfcObjectIds}. Click Add New to create UTRANS roads."; + return; + } + + SetSelectedNewRecords([]); + var selection = await _dfcSelectionService.LoadSelectedAsync(layers); if (selection is null) { ReviewState = null; - ChangeTypeMessage = "Please select one feature from DFC_RESULT layer."; + ChangeTypeMessage = SelectDfcFeatureMessage; StatusMessage = "No DFC_RESULT feature is currently selected."; return; } @@ -293,12 +378,33 @@ private async Task LoadSelectedDfcAsync() catch (Exception exception) { ReviewState = null; + SetSelectedNewRecords([]); ChangeTypeMessage = exception.Message; RemainingDfcRecords = null; StatusMessage = exception.Message; } } + private void SetSelectedNewRecords(IReadOnlyList selections) + { + _selectedNewRecords = selections; + _newRoadValueState = selections.Count switch + { + 0 => null, + 1 => new EditorReviewState(selections[0]), + _ => new EditorReviewState(), + }; + SelectedDfcObjectIds = string.Join( + ", ", + selections.Select(selection => selection.ObjectId) + ); + OnPropertyChanged(nameof(CanAddNew)); + OnPropertyChanged(nameof(CanEditRoadValues)); + OnPropertyChanged(nameof(HasMultipleNewRecords)); + OnPropertyChanged(nameof(DisplayDfcStatus)); + OnPropertyChanged(nameof(RoadValueState)); + } + private async Task LoadCodedValueOptionsAsync(EditorLayerContext layers) { if (_codedValueOptionsLoaded) @@ -310,10 +416,12 @@ private async Task LoadCodedValueOptionsAsync(EditorLayerContext layers) CartocodeValues = options["CARTOCODE"]; OnewayValues = options["ONEWAY"]; VerticalLevelValues = options["VERT_LEVEL"]; + StatusValues = options["STATUS"]; _codedValueOptionsLoaded = true; OnPropertyChanged(nameof(CartocodeValues)); OnPropertyChanged(nameof(OnewayValues)); OnPropertyChanged(nameof(VerticalLevelValues)); + OnPropertyChanged(nameof(StatusValues)); } private static IReadOnlyList CreateRange(int start, int end)