From 38d78cf5957aaa650f7dece2d8464681bddf5992 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Fri, 28 Aug 2026 14:25:08 -0600 Subject: [PATCH 01/11] chore: add feature updates and resolve conflicts --- .github/dependabot.yml | 24 + .../pull_request.attribute-rules.yml | 39 + .vscode/settings.json | 7 +- README.md | 75 +- attribute-rules/.gitignore | 1 + .../Roads_Edit/calculate_dot_owner.arcade | 122 + .../convert_to_null_if_no_value.arcade | 26 + .../Roads_Edit/fullname_calculation.arcade | 28 + .../remove_apostrophes_from_name.arcade | 7 + .../Roads_Edit/remove_extra_whitespace.arcade | 12 + .../Roads_Edit/spatial_assignments.arcade | 47 + .../calculation/Roads_Edit/unique_id.arcade | 126 + .../constrain_to_attribute_domain.arcade | 50 + attribute-rules/deploy_all_ar.py | 158 ++ attribute-rules/deploy_ar.py | 626 +++++ attribute-rules/new_ar.py | 21 + attribute-rules/package.json | 17 + attribute-rules/pnpm-lock.yaml | 2311 +++++++++++++++++ attribute-rules/pnpm-workspace.yaml | 8 + attribute-rules/set_field_defaults.py | 183 ++ attribute-rules/test/_helpers.ts | 22 + .../constrain_to_attribute_domain.test.ts | 41 + .../test/convert_to_null_if_no_value.test.ts | 23 + .../test/remove_apostrophes_from_name.test.ts | 15 + .../test/spatial_calculations.test.ts | 48 + .../test/text_calculations.test.ts | 31 + attribute-rules/tsconfig.json | 4 + attribute-rules/vitest.config.ts | 8 + 28 files changed, 4078 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pull_request.attribute-rules.yml create mode 100644 attribute-rules/.gitignore create mode 100644 attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade create mode 100644 attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade create mode 100644 attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade create mode 100644 attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade create mode 100644 attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade create mode 100644 attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade create mode 100644 attribute-rules/calculation/Roads_Edit/unique_id.arcade create mode 100644 attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade create mode 100644 attribute-rules/deploy_all_ar.py create mode 100644 attribute-rules/deploy_ar.py create mode 100644 attribute-rules/new_ar.py create mode 100644 attribute-rules/package.json create mode 100644 attribute-rules/pnpm-lock.yaml create mode 100644 attribute-rules/pnpm-workspace.yaml create mode 100644 attribute-rules/set_field_defaults.py create mode 100644 attribute-rules/test/_helpers.ts create mode 100644 attribute-rules/test/constrain_to_attribute_domain.test.ts create mode 100644 attribute-rules/test/convert_to_null_if_no_value.test.ts create mode 100644 attribute-rules/test/remove_apostrophes_from_name.test.ts create mode 100644 attribute-rules/test/spatial_calculations.test.ts create mode 100644 attribute-rules/test/text_calculations.test.ts create mode 100644 attribute-rules/tsconfig.json create mode 100644 attribute-rules/vitest.config.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..59381d3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: /attribute-rules + schedule: + interval: quarterly + groups: + safe-dependencies: + update-types: + - minor + - patch + major-dependencies: + update-types: + - major + commit-message: + prefix: deps + prefix-development: deps(dev) + cooldown: + default-days: 10 + semver-major-days: 60 + semver-minor-days: 14 + semver-patch-days: 7 + exclude: + - '@ugrc/*' \ No newline at end of file diff --git a/.github/workflows/pull_request.attribute-rules.yml b/.github/workflows/pull_request.attribute-rules.yml new file mode 100644 index 0000000..d83f022 --- /dev/null +++ b/.github/workflows/pull_request.attribute-rules.yml @@ -0,0 +1,39 @@ +name: Pull Request Events (Attribute Rules) + +on: + pull_request: + paths: + - "attribute-rules/**/*" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Test Arcade Expressions + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Set up code + uses: actions/checkout@v7 + + - name: πŸ“¦ Setup PNPM + uses: pnpm/action-setup@v6 + with: + version: 11 + + - name: βŽ” Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: lts/* + cache: pnpm + cache-dependency-path: attribute-rules/pnpm-lock.yaml + + - name: πŸ“₯ Download dependencies + working-directory: attribute-rules + run: pnpm install --frozen-lockfile + + - name: πŸ§ͺ Run tests + working-directory: attribute-rules + run: pnpm test \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b2b4972..6e57f5b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,14 @@ { "python-envs.defaultEnvManager": "ms-python.python:conda", + "files.associations": { + "*.arcade": "javascript" + }, + "vitest.rootConfig": "${workspaceFolder}/attribute-rules/vitest.config.ts", "cSpell.words": [ "ACCESSCODE", "ALTROADNAME", "ALTROADTYPE", + "arcgis", "arcgispro", "arcpy", "boxelder", @@ -59,7 +64,7 @@ "/^conda activate utrans-tools; pytest[^;]*$/": { "approve": true, "matchCommandLine": true - } + }, "/^conda activate utrans-tools; ruff/": { "approve": true, "matchCommandLine": true diff --git a/README.md b/README.md index 80fd133..fc57636 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,80 @@ Tools for working with UTRANS data -## CLI +## Attribute Rule Scripts + +The attribute-rule utilities are in [attribute-rules/new_ar.py](attribute-rules/new_ar.py), [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py), and [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py). + +- [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py) is the primary script. + - Default mode is idempotent deploy: add if missing, alter if mutable changes, recreate when immutable changes require it. + - Use `--mode add` for strict add-only behavior. + - Use `--mode alter` to update an existing rule only. +- [attribute-rules/new_ar.py](attribute-rules/new_ar.py) is a compatibility wrapper that forwards to deploy script add mode. +- [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py) recursively deploys every `.arcade` file using idempotent deploy mode. It infers the rule type from the top-level folder (`calculation`, `constraint`, or `validation`) and reads optional `// KEY: value` metadata from each script. + +Supported per-script metadata keys are `RULE_NAME`, `FIELD`, `EVENTS`, `IS_EDITABLE`, `ERROR_NUMBER`, `ERROR_MESSAGE`, `DESCRIPTION`, and `SUBTYPE`. Use `FIELD:` with no value for calculation rules that return an `attributes` dictionary for multiple fields. + +Examples: + +```bash +# deploy mode (default) +python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --field FULLNAME + +# add-only compatibility behavior +python attribute-rules/new_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --field FULLNAME + +# alter existing rule only +python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode alter --field FULLNAME + +# recursively deploy every rule using its script metadata +python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" + +# preview all actions without changing the geodatabase +python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" --dry-run +``` + +### Deploying to an enterprise geodatabase + +Attribute rule tools require a file, mobile, or enterprise geodatabase dataset. A feature service URL such as `https://server/server/rest/services/Roads/FeatureServer/0` cannot be used as `--in-table`, and ArcGIS Online hosted feature layers are not a supported target. Deploy to the enterprise geodatabase instead. + +The UTRANS enterprise geodatabase is SQL Server on `db.ugrc.utah.gov`. Create a connection file once, then use its `.sde` path in place of a local `.gdb` path. + +Option 1 β€” ArcGIS Pro: use the **New Database Connection** dialog in the Catalog pane (Database Platform `SQL Server`, Instance `db.ugrc.utah.gov`, Authentication Type `Database authentication`, User name `TRANSADMIN`). Then right-click the finished connection and choose **Copy Path** to get the `.sde` location to pass to the script. Connections saved to Catalog favorites live in `%APPDATA%\Esri\ArcGISPro\Favorites`, so you can also browse to the file directly. + +Option 2 β€” Python: + +```python +import arcpy + +arcpy.management.CreateDatabaseConnection( + out_folder_path="Z:/connections", + out_name="roadtest_transadmin.sde", + database_platform="SQL_SERVER", + instance="db.ugrc.utah.gov", + account_authentication="DATABASE_AUTH", + username="TRANSADMIN", + save_user_pass="SAVE_USERNAME", + database="RoadTest", +) +``` + +Append the dataset name to the connection path and deploy: + +```bash +python attribute-rules/deploy_all_ar.py --in-table "Z:/connections/roadtest.sde/TRANSADMIN.Roads_Edit" --dry-run +``` + +Requirements to be aware of: + +- Connect as the dataset owner (`TRANSADMIN`). Attribute rule changes made by any other account fail validation. +- The identity that matters is the database login stored in the `.sde` file, not your Windows account or ArcGIS Pro portal sign-in. Check it with `arcpy.Describe(".sde").connectionProperties.user`. +- Validation rules require branch versioning on an enterprise geodatabase. Calculation and constraint rules do not. +- Datasets are qualified as `schema.table`. Feature classes inside a feature dataset can also be addressed as `.sde/TRANSADMIN.Centerlines_Edit/TRANSADMIN.Roads_Edit`. +- Always run `--dry-run` first. It previews each rule action and warns about owner and versioning problems, but because it never calls the geoprocessing tool it cannot catch every server-side rejection. +- Confirm a connection's platform with `arcpy.Describe(".sde").connectionProperties.dbclient`. +- Keep connection files outside the repository so database credentials are never committed. + +## `Get_Recent_Edits.py` script run example This is a Python CLI tool for processing county-submitted data prior to ingestion into UTRANS. See its [README](cli/README.md) for installation, usage, and release instructions. diff --git a/attribute-rules/.gitignore b/attribute-rules/.gitignore new file mode 100644 index 0000000..30bc162 --- /dev/null +++ b/attribute-rules/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade new file mode 100644 index 0000000..c0629f6 --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade @@ -0,0 +1,122 @@ +// Calculates DOT_OWN_L and DOT_OWN_R +// FIELD: +// EVENTS: INSERT, UPDATE +Expects($feature, 'DOT_OWN_L', 'DOT_OWN_R', 'DOT_RTNAME'); + +function distance2d(from, to) { + var dx = to.x - from.x; + var dy = to.y - from.y; + + return Sqrt(dx * dx + dy * dy); +} + +// Arcade has no linear referencing, so walk the vertices to find the point halfway along the line. +function midpointAlongLine(geo) { + var paths = geo.paths; + if (IsEmpty(paths) || Count(paths) == 0) { + return null; + } + + var total = 0; + for (var p in paths) { + var path = paths[p]; + for (var v = 1; v < Count(path); v++) { + total += distance2d(path[v - 1], path[v]); + } + } + + var firstInPaths = paths[0][0]; + if (total == 0) { + return { 'x': firstInPaths.x, 'y': firstInPaths.y }; + } + + var target = total / 2; + var traveled = 0; + for (var i in paths) { + var current = paths[i]; + for (var j = 1; j < Count(current); j++) { + var start = current[j - 1]; + var end = current[j]; + var len = distance2d(start, end); + + if (len > 0) { + if (traveled + len >= target) { + var ratio = (target - traveled) / len; + + return { + 'x': start.x + (end.x - start.x) * ratio, + 'y': start.y + (end.y - start.y) * ratio + }; + } + + traveled += len; + } + } + } + + return { 'x': firstInPaths.x, 'y': firstInPaths.y }; +} + +var dotRtName = Trim(IIf(IsEmpty($feature.DOT_RTNAME), '', Text($feature.DOT_RTNAME))); +/* +- If DOT_RTNAME starts with 0, it’s a UDOT street: DOT_OWN is just UDOT +- Else + - If the left muni exists, set it to MU + β€œ β€œ + NAME attribute of the left muni + - how is left muni found? + - Else, if the left county exists, set it to the county’s FIPS_STR + - Else, just set it to blank. +- This is the same for both DOT_OWN_L and DOT_OWN_R +*/ +var dotOwnL = ''; +var dotOwnR = ''; +if (Left(dotRtName, 1) == '0') { + dotOwnL = 'UDOT'; + dotOwnR = 'UDOT'; +} else { + // 1. Get the line geometry and calculate the midpoint (50% along line) + var lineGeom = Geometry($feature); + var midPt = midpointAlongLine(lineGeom); // use the custom midpoint function + + // 2. Create left (-15m) and right (+15m) offset points + // Note: Offset syntax depends on unit settings, but conceptually: + var leftPt = Point({ 'x': midPt.x - 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); + var rightPt = Point({ 'x': midPt.x + 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); + + // 3. Query the Municipalities polygon FeatureSet + var muniFS = FeatureSetByName($datastore, "Municipalities", ["NAME"], true); + var leftMuni = First(Intersects(muniFS, leftPt)); + var rightMuni = First(Intersects(muniFS, rightPt)); + + // 4. Query the Counties polygon FeatureSet + var countyFS = FeatureSetByName($datastore, "Counties", ["NAME"], true); + var leftCounty = First(Intersects(countyFS, leftPt)); + var rightCounty = First(Intersects(countyFS, rightPt)); + + // 5. Determine DOT_OWN_L + if (!IsEmpty(leftMuni)) { + dotOwnL = 'MU ' + leftMuni.NAME; + } else if (!IsEmpty(leftCounty)) { + // Convert NAME as such: KANE -> Kane + dotOwnL = 'CO ' + Proper(leftCounty.NAME); + } else { + dotOwnL = ''; + } + + // 6. Determine DOT_OWN_R + if (!IsEmpty(rightMuni)) { + dotOwnR = 'MU ' + rightMuni.NAME; + } else if (!IsEmpty(rightCounty)) { + dotOwnR = 'CO ' + Proper(rightCounty.NAME); + } else { + dotOwnR = ''; + } +} + +return { + "result": { + "attributes": { + "DOT_OWN_L": dotOwnL, + "DOT_OWN_R": dotOwnR + } + } +}; \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade b/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade new file mode 100644 index 0000000..b39c211 --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade @@ -0,0 +1,26 @@ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, IIf, Buffer */ +// ADDRSYS_L/R are text fields under CVDomain_AddressReferenceSystem, so null is their only valid empty value. +// FIELD: +// EVENTS: INSERT, UPDATE +var textFields = ['ADDRSYS_L', 'ADDRSYS_R', 'BIKE_L', 'BIKE_R', 'BIKE_PLN_L', 'BIKE_PLN_R', 'DOT_HWYNAM', 'DOT_RTNAME', 'DOT_RTPART']; +Expects($feature, 'ADDRSYS_L', 'ADDRSYS_R', 'BIKE_L', 'BIKE_R', 'BIKE_PLN_L', 'BIKE_PLN_R', 'DOT_HWYNAM', 'DOT_RTNAME', 'DOT_RTPART'); +var updates = {}; + +for (var i in textFields) { + var f = textFields[i]; + var val = $feature[f]; + + // Convert empty/whitespace strings to null, leave valid values alone + if (IsEmpty(Trim(val))) { + updates[f] = null; + } else { + updates[f] = val; + } +} + +// For calculation rules, "attributes" must be nested under "result". +return { + "result": { + "attributes": updates + } +}; diff --git a/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade b/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade new file mode 100644 index 0000000..d31f80d --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade @@ -0,0 +1,28 @@ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, IIf, Buffer */ +// FIELD: FULLNAME +// EVENTS: INSERT, UPDATE +function normalizePart(value) { + var part = Trim(IIf(IsEmpty(value), "", Text(value))); + while (Find(" ", part) > -1) { + part = Replace(part, " ", " "); + } + return part; +} + +var name = normalizePart(Replace(IIf(IsEmpty($feature.NAME), "", $feature.NAME), "'", "")); +var postdir = normalizePart($feature.POSTDIR); +var posttype = normalizePart($feature.POSTTYPE); + +var parts = []; +if (!IsEmpty(name)) { + Push(parts, name); +} +if (!IsEmpty(postdir)) { + Push(parts, postdir); +} +if (!IsEmpty(posttype)) { + Push(parts, posttype); +} + +// Test case: NAME=" MAIN ", POSTDIR=null, POSTTYPE=" RD " -> "MAIN RD" +return Concatenate(parts, " "); \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade b/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade new file mode 100644 index 0000000..45fd3f0 --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/remove_apostrophes_from_name.arcade @@ -0,0 +1,7 @@ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, IIf, Buffer */ +// FIELD: NAME +// EVENTS: INSERT, UPDATE +// Remove apostrophes from NAME field on CREATE and UPDATE events +// Test case: NAME="O'CONNELL" -> "OCONNELL" +var name = IIf(IsEmpty($feature.NAME), "", Text($feature.NAME)); +return Replace(name, "'", ""); \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade b/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade new file mode 100644 index 0000000..6a32e99 --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade @@ -0,0 +1,12 @@ +/* global $feature */ +// FIELD: NAME +// EVENTS: INSERT, UPDATE +// Trim NAME and collapse accidental internal whitespace. +// Test case: NAME=" NORTH MAIN " -> "NORTH MAIN" + +var value = Trim(IIf(IsEmpty($feature.NAME), "", Text($feature.NAME))); +while (Find(" ", value) > -1) { + value = Replace(value, " ", " "); +} + +return value; diff --git a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade new file mode 100644 index 0000000..032f0fe --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade @@ -0,0 +1,47 @@ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, IIf, Buffer */ +// FIELD: +// EVENTS: INSERT, UPDATE + +var addressSystemQuadrantsLayer = FeatureSetByName($datastore, "AddressSystemQuadrants", ["QUADRANT", "GRID_NAME"]); +var countyLayer = FeatureSetByName($datastore, "Counties", ["FIPS_STR"]); +var zipCodeLayer = FeatureSetByName($datastore, "ZipCodes", ["ZIP5", "NAME"]); +var municipalityLayer = FeatureSetByName($datastore, "Municipalities", ["NAME"]); + +var roadLeft = Centroid(Offset($feature, -15, 'meters', 'square')); +var roadRight = Centroid(Offset($feature, 15, 'meters', 'square')); + +function fieldOrNull(feature, fieldName) { + if (IsEmpty(feature)) { + return null; + } + + return feature[fieldName]; +} + +var quadrantLeftFeature = First(Intersects(addressSystemQuadrantsLayer, roadLeft)); +var quadrantRightFeature = First(Intersects(addressSystemQuadrantsLayer, roadRight)); +var countyLeftFeature = First(Intersects(countyLayer, roadLeft)); +var countyRightFeature = First(Intersects(countyLayer, roadRight)); +var zipCodeLeftFeature = First(Intersects(zipCodeLayer, roadLeft)); +var zipCodeRightFeature = First(Intersects(zipCodeLayer, roadRight)); +var municipalityLeftFeature = First(Intersects(municipalityLayer, roadLeft)); +var municipalityRightFeature = First(Intersects(municipalityLayer, roadRight)); + +return { + "result": { + "attributes": { + "QUADRANT_L": fieldOrNull(quadrantLeftFeature, "QUADRANT"), + "QUADRANT_R": fieldOrNull(quadrantRightFeature, "QUADRANT"), + "COUNTY_L": fieldOrNull(countyLeftFeature, "FIPS_STR"), + "COUNTY_R": fieldOrNull(countyRightFeature, "FIPS_STR"), + "ADDRSYS_L": fieldOrNull(quadrantLeftFeature, "GRID_NAME"), + "ADDRSYS_R": fieldOrNull(quadrantRightFeature, "GRID_NAME"), + "POSTCOMM_L": fieldOrNull(zipCodeLeftFeature, "NAME"), + "POSTCOMM_R": fieldOrNull(zipCodeRightFeature, "NAME"), + "ZIPCODE_L": fieldOrNull(zipCodeLeftFeature, "ZIP5"), + "ZIPCODE_R": fieldOrNull(zipCodeRightFeature, "ZIP5"), + "INCMUNI_L": fieldOrNull(municipalityLeftFeature, "NAME"), + "INCMUNI_R": fieldOrNull(municipalityRightFeature, "NAME") + } + } +} diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade new file mode 100644 index 0000000..505a61b --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -0,0 +1,126 @@ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, Intersects, First, Point, IIf, Buffer, Expects, Geometry, IsEmpty, Trim, Text, Count, Sqrt, Floor, Find, Mid, Replace */ +// Ports the legacy UtransEditor getNewUniqueID logic, reading GRID1MIL/GRID100K from the USNG. +// Grid-cell zone mismatches at the UTM 11/12 boundary are ignored since this ID is only eyeballed, not parsed. +// FIELD: UNIQUE_ID +// EVENTS: INSERT, UPDATE +Expects($feature, 'NAME', 'POSTDIR', 'POSTTYPE', 'UNIQUE_ID'); + +var DIGITS = '0123456789'; + +function distance2d(from, to) { + var dx = to.x - from.x; + var dy = to.y - from.y; + + return Sqrt(dx * dx + dy * dy); +} + +// Arcade has no linear referencing, so walk the vertices to find the point halfway along the line. +function midpointAlongLine(geometry) { + var paths = geometry.paths; + if (IsEmpty(paths) || Count(paths) == 0) { + return null; + } + + var total = 0; + for (var p in paths) { + var path = paths[p]; + for (var v = 1; v < Count(path); v++) { + total += distance2d(path[v - 1], path[v]); + } + } + + var first = paths[0][0]; + if (total == 0) { + return { 'x': first.x, 'y': first.y }; + } + + var target = total / 2; + var traveled = 0; + for (var i in paths) { + var current = paths[i]; + for (var j = 1; j < Count(current); j++) { + var start = current[j - 1]; + var end = current[j]; + var length = distance2d(start, end); + + if (length > 0) { + if (traveled + length >= target) { + var ratio = (target - traveled) / length; + + return { + 'x': start.x + (end.x - start.x) * ratio, + 'y': start.y + (end.y - start.y) * ratio + }; + } + + traveled += length; + } + } + } + + return { 'x': first.x, 'y': first.y }; +} + +// Rounds half up to match the legacy "+ .5 then cast to long" behavior. +function trimCoordinate(value, leading) { + var digits = Text(Floor(value + 0.5)); + + return Mid(digits, leading, Count(digits) - leading - 1); +} + +function isWholeNumber(value) { + if (value == '') { + return false; + } + + for (var i = 0; i < Count(value); i++) { + if (Find(Mid(value, i, 1), DIGITS) == -1) { + return false; + } + } + + return true; +} + +// NAME/POSTDIR/POSTTYPE are computed directly because attribute rule execution order is not guaranteed, +// so FULLNAME may still hold the pre-edit value. +function nameSuffix() { + var name = Trim(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME))); + if (name == '') { + return ''; + } + + var postdir = Trim(IIf(IsEmpty($feature.POSTDIR), '', Text($feature.POSTDIR))); + var posttype = Trim(IIf(IsEmpty($feature.POSTTYPE), '', Text($feature.POSTTYPE))); + var suffix = IIf(isWholeNumber(name), postdir, posttype); + + var value = '_' + name; + if (suffix != '') { + value += '_' + suffix; + } + + return Replace(value, ' ', '_'); +} + +var geometry = Geometry($feature); +if (IsEmpty(geometry)) { + return "ERR:MISSING_GEOMETRY"; +} + +var midpoint = midpointAlongLine(geometry); +if (IsEmpty(midpoint)) { + return "ERR:MISSING_MIDPOINT"; +} + +var midpointGeom = Point({ 'x': midpoint.x, 'y': midpoint.y, 'spatialReference': geometry.spatialReference }); +var nationalGrid = FeatureSetByName($datastore, 'NationalGrid', ['GRID1MIL', 'GRID100K'], true); +var gridMatch = First(Intersects(nationalGrid, midpointGeom)); +if (IsEmpty(gridMatch)) { + return "ERR:NO_GRID_MATCH"; +} + +var grid1mil = gridMatch.GRID1MIL; +var grid100k = gridMatch.GRID100K; + +// Test case: midpoint 12T VK 3756 1234 on "GOLD KEY LN" -> "12TVK37561234_GOLD_KEY_LN" +return Trim(grid1mil + grid100k + trimCoordinate(midpoint.x, 1) + trimCoordinate(midpoint.y, 2) + nameSuffix()); diff --git a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade new file mode 100644 index 0000000..340d302 --- /dev/null +++ b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade @@ -0,0 +1,50 @@ +// EVENTS: INSERT, UPDATE +// ERROR_NUMBER: 1 +// ERROR_MESSAGE: A field value does not match its coded-value domain. +// Pre-declare fields to the Pro Arcade parser +Expects($feature, + 'PREDIR', 'POSTTYPE', 'POSTDIR', 'AN_POSTDIR', + 'A1_PREDIR', 'A1_POSTTYPE', 'A1_POSTDIR', + 'A2_PREDIR', 'A2_POSTTYPE', 'A2_POSTDIR', + 'VERT_LEVEL', 'ONEWAY', 'SPEED_LMT' +); +// Array of all fields to validate against their assigned domains +var fieldsToCheck = [ + 'PREDIR', 'POSTTYPE', 'POSTDIR', 'AN_POSTDIR', + 'A1_PREDIR', 'A1_POSTTYPE', 'A1_POSTDIR', + 'A2_PREDIR', 'A2_POSTTYPE', 'A2_POSTDIR', + 'VERT_LEVEL', 'ONEWAY', 'SPEED_LMT' +]; + +for (var i in fieldsToCheck) { + var fieldName = fieldsToCheck[i]; + var val = $feature[fieldName]; + + // Skip empty/null values (let mandatory field settings handle null checks) + if (IsEmpty(val)) { continue; } + + // Retrieve the domain assigned to this specific field + var dom = Domain($feature, fieldName); + + // If a domain is attached, validate the code + if (!IsEmpty(dom) && HasKey(dom, 'codedValues')) { + var isValid = false; + var validCodes = dom.codedValues; + + for (var j in validCodes) { + if (validCodes[j].code == val) { + isValid = true; + break; + } + } + + // If the value wasn't found in the domain, trigger the failure immediately + if (!isValid) { + return { + 'errorMessage': 'Invalid value [' + Text(val) + '] entered for field: ' + fieldName + }; + } + } +} + +return true; // All fields passed validation diff --git a/attribute-rules/deploy_all_ar.py b/attribute-rules/deploy_all_ar.py new file mode 100644 index 0000000..3160b26 --- /dev/null +++ b/attribute-rules/deploy_all_ar.py @@ -0,0 +1,158 @@ +"""Deploy every attribute rule script under attribute-rules using script metadata. + +Each .arcade file is assigned a rule type by its first folder: calculation, +constraint, or validation. Optional // KEY: value metadata comments override +the remaining deployment settings without requiring per-rule command-line args. + +USAGE: python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" +""" + +from __future__ import annotations + +import argparse +import re +from dataclasses import dataclass +from pathlib import Path + +from deploy_ar import ATTRIBUTE_RULES_ROOT, main as deploy_rule + + +TYPE_BY_FOLDER = { + "calculation": "CALCULATION", + "constraint": "CONSTRAINT", + "validation": "VALIDATION", +} +METADATA_PATTERN = re.compile(r"^\s*//\s*([A-Z][A-Z0-9_ ]*)\s*:\s*(.*?)\s*$") + + +@dataclass(frozen=True) +class RuleDeployment: + arcade_path: Path + rule_type: str + metadata: dict[str, str] + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Idempotently deploy every .arcade attribute rule under attribute-rules.", + ) + parser.add_argument( + "--in-table", + required=True, + help="Target feature class or table where all rules will be managed.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview each deploy action without writing changes.", + ) + parser.add_argument( + "--no-recreate", + action="store_true", + help="Fail rather than delete and recreate a rule with immutable differences.", + ) + return parser.parse_args(argv) + + +def _read_metadata(arcade_path: Path) -> dict[str, str]: + metadata: dict[str, str] = {} + for line in arcade_path.read_text(encoding="utf-8").splitlines(): + match = METADATA_PATTERN.match(line) + if match is None: + continue + key = re.sub(r"\s+", "_", match.group(1).strip()) + metadata[key] = match.group(2).strip() + return metadata + + +def _rule_type_for_path(arcade_path: Path) -> str: + try: + category = arcade_path.relative_to(ATTRIBUTE_RULES_ROOT).parts[0].casefold() + except (ValueError, IndexError) as error: + raise ValueError(f"Cannot determine rule type for {arcade_path}") from error + + try: + return TYPE_BY_FOLDER[category] + except KeyError as error: + allowed = ", ".join(sorted(TYPE_BY_FOLDER)) + raise ValueError( + f"{arcade_path} must be below one of these folders: {allowed}" + ) from error + + +def _discover_rules() -> list[RuleDeployment]: + rules: list[RuleDeployment] = [] + for arcade_path in sorted(ATTRIBUTE_RULES_ROOT.rglob("*.arcade")): + rules.append( + RuleDeployment( + arcade_path=arcade_path, + rule_type=_rule_type_for_path(arcade_path), + metadata=_read_metadata(arcade_path), + ) + ) + if not rules: + raise RuntimeError(f"No .arcade scripts found under {ATTRIBUTE_RULES_ROOT}") + return rules + + +def _normalize_events(events: str) -> str: + return ";".join(part.strip().upper() for part in re.split(r"[;,]", events) if part.strip()) + + +def _deployment_argv(deployment: RuleDeployment, args: argparse.Namespace) -> list[str]: + metadata = deployment.metadata + folder = deployment.arcade_path.parent.relative_to(ATTRIBUTE_RULES_ROOT).as_posix() + argv = [ + deployment.arcade_path.name, + "--in-table", + args.in_table, + "--mode", + "deploy", + "--folder", + folder, + "--type", + metadata.get("TYPE", deployment.rule_type).upper(), + ] + + if metadata.get("TYPE", deployment.rule_type).upper() != deployment.rule_type: + raise ValueError( + f"{deployment.arcade_path}: TYPE must match its {deployment.rule_type.lower()} folder" + ) + + for metadata_key, option in ( + ("RULE_NAME", "--rule-name"), + ("FIELD", "--field"), + ("EVENTS", "--triggering-events"), + ("IS_EDITABLE", "--is-editable"), + ("ERROR_NUMBER", "--error-number"), + ("ERROR_MESSAGE", "--error-message"), + ("DESCRIPTION", "--description"), + ("SUBTYPE", "--subtype"), + ): + if metadata_key in metadata: + value = metadata[metadata_key] + if metadata_key == "EVENTS": + value = _normalize_events(value) + argv.extend([option, value]) + + if args.dry_run: + argv.append("--dry-run") + if args.no_recreate: + argv.append("--no-recreate") + return argv + + +def main(argv: list[str] | None = None) -> None: + args = _parse_args(argv) + rules = _discover_rules() + + # Build every command before changing the target so invalid metadata fails cleanly. + deployments = [(rule, _deployment_argv(rule, args)) for rule in rules] + print(f"Deploying {len(deployments)} attribute rule(s) to: {args.in_table}", flush=True) + for deployment, deploy_argv in deployments: + print(f"\n=== {deployment.arcade_path.relative_to(ATTRIBUTE_RULES_ROOT)} ===", flush=True) + deploy_rule(deploy_argv) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/attribute-rules/deploy_ar.py b/attribute-rules/deploy_ar.py new file mode 100644 index 0000000..02cc586 --- /dev/null +++ b/attribute-rules/deploy_ar.py @@ -0,0 +1,626 @@ +"""Deploy ArcGIS attribute rules from local .arcade scripts. + +Examples: + # idempotent deploy (add if missing, update if present) + python attribute-rules/deploy_ar.py fullname_calculation \ + --in-table "Z:/data/County.gdb/Roads" \ + --field FULLNAME + + # strict add-only behavior + python attribute-rules/deploy_ar.py fullname_calculation \ + --in-table "Z:/data/County.gdb/Roads" \ + --mode add +""" + +from __future__ import annotations + +import argparse +import re +from dataclasses import dataclass +from pathlib import Path + +import arcpy + + +ATTRIBUTE_RULES_ROOT = Path(__file__).resolve().parent + + +@dataclass(frozen=True) +class RuleState: + name: str + type: str + field: str + subtype: str + is_editable: str + triggering_events: str + script_expression: str + error_number: str + error_message: str + description: str + + +def _log(message: str) -> None: + # Flush so progress is visible during slow enterprise deploys and when output is piped. + print(message, flush=True) + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Deploy an ArcGIS attribute rule from a .arcade script in the attribute-rules directory." + ), + epilog=( + "Modes:\n" + "- deploy (default): add if missing, alter if mutable changes, recreate if immutable changes.\n" + "- add: add only and fail when the rule already exists.\n" + "- alter: alter only and fail when the rule is missing.\n" + "\n" + "Notes:\n" + "- Required inputs: script_name and --in-table.\n" + "- script_name can be either name or name.arcade.\n" + "- If --folder is omitted, the script is searched recursively under attribute-rules." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + ) + required_args = parser.add_argument_group("required arguments") + optional_args = parser.add_argument_group("optional arguments") + + optional_args.add_argument( + "-h", + "--help", + action="help", + help="show this help message and exit", + ) + parser.add_argument( + "script_name", + help="Arcade script filename with or without .arcade extension.", + ) + required_args.add_argument( + "--in-table", + required=True, + help="Target feature class or table where the rule will be managed.", + ) + optional_args.add_argument( + "--mode", + default="deploy", + choices=["deploy", "add", "alter"], + help="Rule management mode.", + ) + optional_args.add_argument( + "--folder", + default="", + help="Optional subfolder under attribute-rules that contains the .arcade file.", + ) + optional_args.add_argument( + "--rule-name", + help="Optional explicit rule name. Defaults to a title-cased name from the script filename.", + ) + optional_args.add_argument( + "--dry-run", + action="store_true", + help="Preview actions without writing changes.", + ) + optional_args.add_argument( + "--no-recreate", + action="store_true", + help=( + "In deploy mode, fail instead of delete/recreate when immutable properties differ." + ), + ) + + optional_args.add_argument( + "--type", + default="CALCULATION", + choices=["CALCULATION", "CONSTRAINT", "VALIDATION"], + help="Attribute rule type.", + ) + optional_args.add_argument( + "--field", + default="", + help="Target field for the rule when applicable (commonly used for CALCULATION).", + ) + optional_args.add_argument( + "--triggering-events", + default="INSERT;UPDATE", + help="Semicolon-delimited events, e.g. INSERT;UPDATE or UPDATE.", + ) + optional_args.add_argument( + "--is-editable", + default="EDITABLE", + choices=["EDITABLE", "NONEDITABLE"], + help="Whether users can manually edit values managed by the rule.", + ) + optional_args.add_argument( + "--error-number", + type=int, + default=1, + help="Error number for CONSTRAINT/VALIDATION rules.", + ) + optional_args.add_argument( + "--error-message", + default="Attribute rule violation.", + help="Error message for CONSTRAINT/VALIDATION rules.", + ) + optional_args.add_argument( + "--description", + default="", + help="Optional attribute rule description.", + ) + optional_args.add_argument( + "--subtype", + default="", + help="Optional subtype code.", + ) + + return parser.parse_args(argv) + + +def _normalize_script_name(script_name: str) -> str: + script_path = Path(script_name) + stem = script_path.stem + if not stem: + raise ValueError("script_name must not be empty") + return f"{stem}.arcade" + + +def _default_rule_name_from_stem(stem: str) -> str: + # Convert snake/kebab/camel-ish names into user-friendly ArcGIS rule names. + spaced = re.sub(r"[_-]+", " ", stem) + spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", spaced) + return " ".join(word.capitalize() for word in spaced.split()) + + +def _normalize_rule_name_for_lookup(name: str) -> str: + # ArcGIS treats rule names effectively case-insensitively; normalize lookups for idempotency. + return _as_text(name).strip().casefold() + + +def _resolve_arcade_path(script_name: str, folder: str) -> Path: + arcade_filename = _normalize_script_name(script_name) + if folder: + candidate = (ATTRIBUTE_RULES_ROOT / folder / arcade_filename).resolve() + if not candidate.is_relative_to(ATTRIBUTE_RULES_ROOT): + raise ValueError("Resolved script path must stay within the attribute-rules directory") + if not candidate.exists(): + raise FileNotFoundError(f"Arcade script not found: {candidate}") + return candidate + + matches = sorted(path.resolve() for path in ATTRIBUTE_RULES_ROOT.rglob(arcade_filename)) + if not matches: + raise FileNotFoundError( + f"Arcade script '{arcade_filename}' was not found under {ATTRIBUTE_RULES_ROOT}" + ) + if len(matches) > 1: + match_list = "\n".join(str(path) for path in matches) + raise RuntimeError( + "Multiple matching scripts found. Use --folder to disambiguate:\n" + f"{match_list}" + ) + + return matches[0] + + +def _as_text(value: object) -> str: + if value is None: + return "" + return str(value) + + +def _normalize_type(value: object) -> str: + text = _as_text(value).strip().upper() + if text.startswith("ESRIART"): + return text.removeprefix("ESRIART") + return text + + +def _normalize_subtype(value: object) -> str: + text = _as_text(value).strip() + if text in {"", "-1"}: + return "" + return text + + +def _normalize_events(value: object) -> str: + if isinstance(value, (list, tuple, set)): + raw_parts = [_as_text(part) for part in value] + else: + text = _as_text(value) + if "[" in text and "]" in text: + raw_parts = re.findall(r"[A-Za-z0-9_]+", text) + else: + raw_parts = re.split(r"[;,]", text) + + events: list[str] = [] + for part in raw_parts: + token = part.strip().strip("\"'").upper() + if not token: + continue + if token.startswith("ESRIARTE"): + token = token.removeprefix("ESRIARTE") + events.append(token) + + return ";".join(sorted(set(events))) + + +def _normalize_editable(value: object) -> str: + if isinstance(value, bool): + return "EDITABLE" if value else "NONEDITABLE" + text = _as_text(value).strip().upper() + if text in {"TRUE", "1", "EDITABLE"}: + return "EDITABLE" + if text in {"FALSE", "0", "NONEDITABLE"}: + return "NONEDITABLE" + return text + + +def _normalize_script(script_expression: str) -> str: + return script_expression.replace("\r\n", "\n").strip() + + +def _rule_state_from_args(args: argparse.Namespace, rule_name: str, script_text: str) -> RuleState: + type_name = _normalize_type(args.type) + triggering_events = _normalize_events(args.triggering_events) + if type_name == "VALIDATION": + triggering_events = "" + + error_number = _as_text(args.error_number).strip() + error_message = _as_text(args.error_message) + if type_name == "CALCULATION": + # ArcGIS commonly stores calc-rule error metadata as unset/sentinel values. + error_number = "-1" + error_message = "" + + return RuleState( + name=rule_name, + type=type_name, + field=_as_text(args.field).strip(), + subtype=_normalize_subtype(args.subtype), + is_editable=_normalize_editable(args.is_editable), + triggering_events=triggering_events, + script_expression=_normalize_script(script_text), + error_number=error_number, + error_message=error_message, + description=_as_text(args.description), + ) + + +def _rule_state_from_existing(rule: object) -> RuleState: + return RuleState( + name=_as_text(getattr(rule, "name", "")).strip(), + type=_normalize_type(getattr(rule, "type", "")), + field=_as_text(getattr(rule, "fieldName", "")).strip(), + subtype=_normalize_subtype(getattr(rule, "subtypeCode", "")), + is_editable=_normalize_editable(getattr(rule, "isEditable", "")), + triggering_events=_normalize_events(getattr(rule, "triggeringEvents", "")), + script_expression=_normalize_script(_as_text(getattr(rule, "scriptExpression", ""))), + error_number=_as_text(getattr(rule, "errorNumber", "")).strip(), + error_message=_as_text(getattr(rule, "errorMessage", "")), + description=_as_text(getattr(rule, "description", "")), + ) + + +# Describe round trips cost seconds against an enterprise geodatabase, so cache static lookups. +_DESCRIBE_CACHE: dict[str, object] = {} + + +def _describe_cached(path: str) -> object: + if path not in _DESCRIBE_CACHE: + _DESCRIBE_CACHE[path] = arcpy.Describe(path) + return _DESCRIBE_CACHE[path] + + +def _validate_in_table(in_table: str) -> None: + data_type = _as_text(getattr(_describe_cached(in_table), "dataType", "")) + if data_type in {"FeatureClass", "Table", "FeatureLayer", "TableView"}: + return + + if data_type in {"Workspace", "FeatureDataset"}: + raise RuntimeError( + f"--in-table must name a feature class or table, but {in_table} is a " + f"{data_type}. Append the dataset name, for example " + f"{in_table}\\TRANSADMIN.Roads_Edit" + ) + + raise RuntimeError(f"--in-table must be a feature class or table, but found {data_type}: {in_table}") + + +def _describe_workspace(in_table: str) -> object | None: + path = _as_text(getattr(_describe_cached(in_table), "path", "")) + # Walk up past any feature dataset until the workspace itself is described. + for _ in range(3): + if not path: + return None + desc = _describe_cached(path) + if hasattr(desc, "connectionProperties"): + return desc + path = _as_text(getattr(desc, "path", "")) + return None + + +def _warn_enterprise_preconditions(in_table: str, rule_type: str) -> None: + workspace = _describe_workspace(in_table) + if workspace is None: + return + + desc = _describe_cached(in_table) + connection = getattr(workspace, "connectionProperties", None) + + if ( + rule_type == "VALIDATION" + and getattr(desc, "isVersioned", False) + and not getattr(connection, "branch", None) + ): + _log( + "WARNING: validation rules require branch versioning on an enterprise " + "geodatabase, but this dataset is not branch versioned." + ) + + owner = _as_text(getattr(desc, "name", "")).split(".")[0] + user = _as_text(getattr(connection, "user", "")) + if owner and user and owner.casefold() != user.casefold(): + _log( + f"WARNING: connected as {user!r} but the dataset owner is {owner!r}. " + "Attribute rule changes must be made by the data owner." + ) + + +def _get_rule_states(in_table: str) -> dict[str, RuleState]: + desc = arcpy.Describe(in_table) + rules = getattr(desc, "attributeRules", None) or [] + states: dict[str, RuleState] = {} + for rule in rules: + state = _rule_state_from_existing(rule) + states[_normalize_rule_name_for_lookup(state.name)] = state + return states + + +def _add_attribute_rule(args: argparse.Namespace, script_text: str, rule_name: str) -> None: + arcpy.management.AddAttributeRule( + in_table=args.in_table, + name=rule_name, + type=args.type, + script_expression=script_text, + is_editable=args.is_editable, + triggering_events=args.triggering_events, + error_number=args.error_number, + error_message=args.error_message, + description=args.description, + subtype=args.subtype, + field=args.field, + ) + + +def _alter_attribute_rule(args: argparse.Namespace, script_text: str, rule_name: str) -> None: + triggering_events = args.triggering_events if args.type != "VALIDATION" else "" + error_number = str(args.error_number) if args.type != "CALCULATION" else "" + error_message = args.error_message if args.type != "CALCULATION" else "" + description = args.description if args.description else "" + + arcpy.management.AlterAttributeRule( + in_table=args.in_table, + name=rule_name, + description=description, + error_number=error_number, + error_message=error_message, + triggering_events=triggering_events, + script_expression=script_text, + ) + + +def _delete_attribute_rule( + args: argparse.Namespace, + rule_name: str, + rule_type: str, +) -> None: + arcpy.management.DeleteAttributeRule(args.in_table, rule_name, rule_type) + + +def _diffs(existing: RuleState, desired: RuleState) -> tuple[list[str], list[str]]: + immutable_diffs: list[str] = [] + mutable_diffs: list[str] = [] + + if existing.type != desired.type: + immutable_diffs.append(f"type: existing={existing.type!r} desired={desired.type!r}") + if existing.field != desired.field: + immutable_diffs.append(f"field: existing={existing.field!r} desired={desired.field!r}") + if existing.subtype != desired.subtype: + immutable_diffs.append(f"subtype: existing={existing.subtype!r} desired={desired.subtype!r}") + if existing.is_editable and desired.is_editable and existing.is_editable != desired.is_editable: + immutable_diffs.append( + f"is_editable: existing={existing.is_editable!r} desired={desired.is_editable!r}" + ) + + if existing.triggering_events != desired.triggering_events: + mutable_diffs.append( + f"triggering_events: existing={existing.triggering_events!r} desired={desired.triggering_events!r}" + ) + if existing.script_expression != desired.script_expression: + mutable_diffs.append("script_expression differs") + if existing.error_number != desired.error_number: + mutable_diffs.append( + f"error_number: existing={existing.error_number!r} desired={desired.error_number!r}" + ) + if existing.error_message != desired.error_message: + mutable_diffs.append( + f"error_message: existing={existing.error_message!r} desired={desired.error_message!r}" + ) + if existing.description != desired.description: + mutable_diffs.append( + f"description: existing={existing.description!r} desired={desired.description!r}" + ) + + return immutable_diffs, mutable_diffs + + +def _log_deploy_context( + args: argparse.Namespace, + arcade_path: Path, + rule_name: str, + desired: RuleState, + existing: RuleState | None, + immutable_diffs: list[str], + mutable_diffs: list[str], +) -> None: + mode = "Dry run" if args.dry_run else "Apply" + _log(f"{mode}: {'no changes will be written' if args.dry_run else 'changes will be applied'}") + _log(f"- mode: {args.mode}") + _log(f"- in_table: {args.in_table}") + _log(f"- arcade_path: {arcade_path}") + _log(f"- rule_name: {rule_name}") + _log(f"- type: {desired.type}") + _log(f"- field: {desired.field or '(none)'}") + _log(f"- subtype: {desired.subtype or '(none)'}") + _log(f"- is_editable: {desired.is_editable}") + _log(f"- triggering_events: {desired.triggering_events or '(none)'}") + _log(f"- existing_rule_found: {'yes' if existing else 'no'}") + + if existing: + if not immutable_diffs and not mutable_diffs: + _log("- diff: no changes") + else: + for item in immutable_diffs: + _log(f"- immutable_diff: {item}") + for item in mutable_diffs: + _log(f"- mutable_diff: {item}") + + +def _run_add_mode( + args: argparse.Namespace, + rule_name: str, + script_text: str, + existing: RuleState | None, +) -> None: + if existing: + raise RuntimeError( + f"Attribute rule '{rule_name}' already exists on {args.in_table}. " + "Choose a different --rule-name or run deploy mode." + ) + + if args.dry_run: + _log(f"WOULD ADD: {rule_name}") + return + + _log(f"Adding rule '{rule_name}' from script") + _add_attribute_rule(args, script_text, rule_name) + _log(f"Successfully added attribute rule '{rule_name}' to: {args.in_table}") + + +def _run_alter_mode( + args: argparse.Namespace, + rule_name: str, + script_text: str, + existing: RuleState | None, + immutable_diffs: list[str], + mutable_diffs: list[str], +) -> None: + if not existing: + raise RuntimeError( + f"Attribute rule '{rule_name}' does not exist on {args.in_table}. " + "Use --mode deploy or --mode add instead." + ) + if immutable_diffs: + raise RuntimeError( + "Alter mode cannot apply immutable changes:\n" + + "\n".join(f"- {item}" for item in immutable_diffs) + ) + if not mutable_diffs: + _log(f"No-op: rule '{rule_name}' already matches desired state") + return + + if args.dry_run: + _log(f"WOULD ALTER: {rule_name}") + return + + _log(f"Altering rule '{rule_name}'") + _alter_attribute_rule(args, script_text, rule_name) + _log(f"Successfully altered attribute rule '{rule_name}' on: {args.in_table}") + + +def _run_deploy_mode( + args: argparse.Namespace, + rule_name: str, + script_text: str, + existing: RuleState | None, + immutable_diffs: list[str], + mutable_diffs: list[str], +) -> None: + if not existing: + if args.dry_run: + _log(f"WOULD ADD: {rule_name}") + return + _log(f"Deploy action: ADD '{rule_name}'") + _add_attribute_rule(args, script_text, rule_name) + _log(f"Successfully added attribute rule '{rule_name}' to: {args.in_table}") + return + + if immutable_diffs: + if args.no_recreate: + raise RuntimeError( + "Immutable differences found and --no-recreate was specified:\n" + + "\n".join(f"- {item}" for item in immutable_diffs) + ) + if args.dry_run: + _log(f"WOULD RECREATE: {rule_name}") + return + _log(f"Deploy action: RECREATE '{rule_name}' (delete + add)") + _delete_attribute_rule(args, rule_name, existing.type) + _add_attribute_rule(args, script_text, rule_name) + _log(f"Successfully recreated attribute rule '{rule_name}' on: {args.in_table}") + return + + if not mutable_diffs: + _log(f"No-op: rule '{rule_name}' already matches desired state") + return + + if args.dry_run: + _log(f"WOULD ALTER: {rule_name}") + return + + _log(f"Deploy action: ALTER '{rule_name}'") + _alter_attribute_rule(args, script_text, rule_name) + _log(f"Successfully altered attribute rule '{rule_name}' on: {args.in_table}") + + +def main(argv: list[str] | None = None) -> None: + args = _parse_args(argv) + + if not arcpy.Exists(args.in_table): + raise RuntimeError(f"Target dataset does not exist: {args.in_table}") + + _validate_in_table(args.in_table) + _warn_enterprise_preconditions(args.in_table, _normalize_type(args.type)) + + arcade_path = _resolve_arcade_path(args.script_name, args.folder) + rule_name = args.rule_name or _default_rule_name_from_stem(arcade_path.stem) + script_text = arcade_path.read_text(encoding="utf-8") + + rules = _get_rule_states(args.in_table) + existing = rules.get(_normalize_rule_name_for_lookup(rule_name)) + desired = _rule_state_from_args(args, rule_name, script_text) + immutable_diffs: list[str] = [] + mutable_diffs: list[str] = [] + if existing is not None: + immutable_diffs, mutable_diffs = _diffs(existing, desired) + + _log_deploy_context( + args=args, + arcade_path=arcade_path, + rule_name=rule_name, + desired=desired, + existing=existing, + immutable_diffs=immutable_diffs, + mutable_diffs=mutable_diffs, + ) + + if args.mode == "add": + _run_add_mode(args, rule_name, script_text, existing) + return + if args.mode == "alter": + _run_alter_mode(args, rule_name, script_text, existing, immutable_diffs, mutable_diffs) + return + _run_deploy_mode(args, rule_name, script_text, existing, immutable_diffs, mutable_diffs) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/attribute-rules/new_ar.py b/attribute-rules/new_ar.py new file mode 100644 index 0000000..2697031 --- /dev/null +++ b/attribute-rules/new_ar.py @@ -0,0 +1,21 @@ +"""Compatibility wrapper for add-only attribute rule behavior. + +For new deployments, use deploy_ar.py directly. +""" + +from __future__ import annotations + +import sys + +from deploy_ar import main as deploy_main + + +def main() -> None: + argv = sys.argv[1:] + if "--mode" in argv: + raise RuntimeError("new_ar.py does not accept --mode. Use deploy_ar.py for mode selection.") + deploy_main(["--mode", "add", *argv]) + + +if __name__ == "__main__": + main() diff --git a/attribute-rules/package.json b/attribute-rules/package.json new file mode 100644 index 0000000..f0c67e8 --- /dev/null +++ b/attribute-rules/package.json @@ -0,0 +1,17 @@ +{ + "name": "utrans-tools-attribute-rules-testing-framework", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run" + }, + "dependencies": { + "@arcgis/core": "^5.1.17" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@ugrc/tsconfigs": "1.0.4", + "typescript": "^5.9.0", + "vitest": "^3.2.4" + } +} \ No newline at end of file diff --git a/attribute-rules/pnpm-lock.yaml b/attribute-rules/pnpm-lock.yaml new file mode 100644 index 0000000..158ed24 --- /dev/null +++ b/attribute-rules/pnpm-lock.yaml @@ -0,0 +1,2311 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@arcgis/core': + specifier: ^5.1.17 + version: 5.1.19 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.13.3 + '@ugrc/tsconfigs': + specifier: 1.0.4 + version: 1.0.4 + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@24.13.3) + +packages: + + '@amcharts/amcharts5@5.18.0': + resolution: {integrity: sha512-2pIka1IrHO0mnTI7A9haq8aaq8leZZhFDythxNA7qYNg/iyGWuNcuzoyzJkybJLlNSlR5crkeNqZQL5up+IF3A==} + + '@arcgis/core@5.1.19': + resolution: {integrity: sha512-8qtHnd+TzfqqKnNRYQha1iM/V5r0qf897pub2r7EoTPER4Cae7kf2GovyAqG4P4TpZ4/Xzz1qyRwdAycsY9FDA==} + + '@arcgis/lumina@5.1.19': + resolution: {integrity: sha512-Vv6DW7jMEBHZgOxlEkGJVz5TRsCRjj+rZeM0bJNtE5XFZI0XJWFDjHJp/cTVBuKbdCu3FjkQn7Swxg2sAc9i2w==} + peerDependencies: + '@lit/context': ^1.1.6 + lit: ^3.3.0 + peerDependenciesMeta: + '@lit/context': + optional: true + + '@arcgis/toolkit@5.1.19': + resolution: {integrity: sha512-F/GpJAzwmJp7mQSttZr6C1+UCqdIKgRKkf6YLT49rmn2aPk5iKObukXtoFQ7CrKnSarakwahi94/DTNKosuLYw==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esri/arcgis-html-sanitizer@4.1.0': + resolution: {integrity: sha512-einEveDJ/k1180NOp78PB/4Hje9eBy3dyOGLLtLn6bSkizpUfCwuYBIXOA7Y3F/k/BsTQXgKqUVwQ0eiscWMdA==} + engines: {node: '>=18.0.0'} + + '@esri/calcite-components@5.1.2': + resolution: {integrity: sha512-KdCk68T+UzVeAMA+kHW5UZ8msedBAH10k1J9Vkhqc8DtpJ0L/j2rw0ve9pbq1sW+hAAbhnTj+0OyP3R5bp1dEg==} + + '@esri/calcite-ui-icons@4.5.0': + resolution: {integrity: sha512-MuXK8o5qefXXV/vdDucQUm8zno+9Q0KjadCX82gYX1VgHMLPp0x2+pbQzG3c0MDbRqkidhTqL/zwq/eVq282qg==} + hasBin: true + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@interactjs/types@1.10.28': + resolution: {integrity: sha512-vPmu4HWmsg0Fub3BPKGk7DuozkhgVK84TVkziY47v8Uh0A14gph55/vVRaQN4oZov0lGNJNK6Jyddim2qd/4vw==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@open-wc/dedupe-mixin@1.4.0': + resolution: {integrity: sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==} + + '@rollup/rollup-android-arm-eabi@4.62.5': + resolution: {integrity: sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.5': + resolution: {integrity: sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.5': + resolution: {integrity: sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.5': + resolution: {integrity: sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.5': + resolution: {integrity: sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.5': + resolution: {integrity: sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.5': + resolution: {integrity: sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.5': + resolution: {integrity: sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.5': + resolution: {integrity: sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.5': + resolution: {integrity: sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.5': + resolution: {integrity: sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.5': + resolution: {integrity: sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.5': + resolution: {integrity: sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.5': + resolution: {integrity: sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.5': + resolution: {integrity: sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.5': + resolution: {integrity: sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.5': + resolution: {integrity: sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.5': + resolution: {integrity: sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.5': + resolution: {integrity: sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.5': + resolution: {integrity: sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.5': + resolution: {integrity: sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.5': + resolution: {integrity: sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.5': + resolution: {integrity: sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.5': + resolution: {integrity: sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.5': + resolution: {integrity: sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@total-typescript/tsconfig@1.0.4': + resolution: {integrity: sha512-fO4ctMPGz1kOFOQ4RCPBRBfMy3gDn+pegUfrGyUFRMv/Rd0ZM3/SHH3hFCYG4u6bPLG8OlmOGcBLDexvyr3A5w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + + '@types/d3-hierarchy@3.1.1': + resolution: {integrity: sha512-QwjxA3+YCKH3N1Rs3uSiSy1bdxlLB1uUiENXeJudBoAFvtDuswUxLcanoOaR2JYn1melDTuIXR8VhnVyI3yG/A==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@1.0.11': + resolution: {integrity: sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} + + '@types/d3-sankey@0.11.2': + resolution: {integrity: sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@1.3.12': + resolution: {integrity: sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/polylabel@1.1.3': + resolution: {integrity: sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==} + + '@types/sortablejs@1.15.9': + resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==} + + '@types/svg-arc-to-cubic-bezier@3.2.3': + resolution: {integrity: sha512-UNOnbTtl0nVTm8hwKaz5R5VZRvSulFMGojO5+Q7yucKxBoCaTtS4ibSQVRHo5VW5AaRo145U8p1Vfg5KrYe9Bg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@ugrc/tsconfigs@1.0.4': + resolution: {integrity: sha512-8ddPHD3C4OgIm/rbMQQreByOgrXBiWA7rOPDBeH3kW+tC2KcrLynCagweTXdP7RZlo8qEG7CHP3fn9dJeUgCTQ==} + + '@vaadin/a11y-base@25.1.11': + resolution: {integrity: sha512-TfnJy4WWQXG+Q3SABH+ZtiHxMZ7CIEP4l0xqq0QVEau/vjmmavLhC/6b24cuRqZnTZbE04hLMm/E5TOrE4ZpyA==} + + '@vaadin/checkbox@25.1.11': + resolution: {integrity: sha512-li80mUZuX8KAs/y+RpmMP9awev7ngwS8M4qsxe9caYligWNX94ftkQrvVLukFhLsfYCKOqy42DUKoZZEYmL6qg==} + + '@vaadin/component-base@25.1.11': + resolution: {integrity: sha512-q5367Y0jXEtlaGcDsJphR4vlVp4kzj5BOBfxGNFsU3dDx9rUWoL/Ac+Kg1kXcqHEySr7sc23lMQSm+sBz9vx/w==} + + '@vaadin/field-base@25.1.11': + resolution: {integrity: sha512-1SMqqbs9fQhnRXXQROmfd+hLQRwMB6+b+Serg9RgoG9srbvzj6oPgEcM4JOdo8BE3HGGwbc2qD1YlDpSG7UT8w==} + + '@vaadin/grid@25.1.11': + resolution: {integrity: sha512-3xY0Lk0Ik2NXS3qSVI/szoNhFlYvTgOQU7OMJC8yWXGOGxVICCR2DKnQoW40sEsBFvw/yPj+ZRW6QeJNq00lmA==} + + '@vaadin/input-container@25.1.11': + resolution: {integrity: sha512-io8+p9uHfB+I9fsy5O9v/0FHNVeQz82GeOnehxs1Ob8LT27PEzlYRdbTV4v/zeh6k+RCGuMG9A3JaD+3btbYfQ==} + + '@vaadin/lit-renderer@25.1.11': + resolution: {integrity: sha512-g4gZNUXFCN3w5kzW39LJcWZARVl3BWtEDklcIytWF6ia9dF8zNwlOSA3mzFFwWG/qbjGM60mk3+lEq/SC0SLIA==} + + '@vaadin/text-field@25.1.11': + resolution: {integrity: sha512-a12FRnE+hlrENTj82D+J4A55e9HS7rwcypAW8jU9pUll5jsWHbBNLsYns7cME2lDfjd5RLBBpDQXcF9dKWLANg==} + + '@vaadin/vaadin-development-mode-detector@2.0.7': + resolution: {integrity: sha512-9FhVhr0ynSR3X2ao+vaIEttcNU5XfzCbxtmYOV8uIRnUCtNgbvMOIcyGBvntsX9I5kvIP2dV3cFAOG9SILJzEA==} + + '@vaadin/vaadin-themable-mixin@25.1.11': + resolution: {integrity: sha512-NYgy4DUmSm6dsJVsi3LyzCJCxwHi2GEX4YdwPPU6fEXAmf7FcF4yW70XI7l9YC2Ep+VeGIiMUAzpqI4/zdaE1g==} + + '@vaadin/vaadin-usage-statistics@2.1.3': + resolution: {integrity: sha512-8r4TNknD7OJQADe3VygeofFR7UNAXZ2/jjBFP5dgI8+2uMfnuGYgbuHivasKr9WSQ64sPej6m8rDoM1uSllXjQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + '@zip.js/zip.js@2.8.54': + resolution: {integrity: sha512-Nr2JRdMBxmdh6v8JYb/BBipvTvBr3Ztwf6+bIdVxb1EMaNNm5gsM9R80pB+c9S2WMdODb3LIWiXfYekDA3+JSw==} + engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@2.1.1: + resolution: {integrity: sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + composed-offset-position@0.0.6: + resolution: {integrity: sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==} + peerDependencies: + '@floating-ui/utils': ^0.2.5 + + cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@2.0.0: + resolution: {integrity: sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@2.0.0: + resolution: {integrity: sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@2.0.0: + resolution: {integrity: sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-voronoi-map@2.1.1: + resolution: {integrity: sha512-mCXfz/kD9IQxjHaU2IMjkO8fSo4J6oysPR2iL+omDsCy1i1Qn6BQ/e4hEAW8C6ms2kfuHwqtbNom80Hih94YsA==} + + d3-voronoi-treemap@1.1.2: + resolution: {integrity: sha512-7odu9HdG/yLPWwzDteJq4yd9Q/NwgQV7IE/u36VQtcCK7k1sZwDqbkHCeMKNTBsq5mQjDwolTsrXcU0j8ZEMCA==} + + d3-weighted-voronoi@1.1.3: + resolution: {integrity: sha512-C3WdvSKl9aqhAy+f3QT3PPsQG6V+ajDfYO3BSclQDSD+araW2xDBFIH67aKzsSuuuKaX8K2y2dGq1fq/dWTVig==} + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + flatpickr@4.6.13: + resolution: {integrity: sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==} + + focus-trap@8.2.2: + resolution: {integrity: sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==} + + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + interactjs@1.10.28: + resolution: {integrity: sha512-QCa4ksTPd2p/FZ4I6hrH6fogjLqru1TxYywRiKYNuLZtrlAxTIYUa/MYsE7PnUgWZJU3SrGVDjiz065BeaXYng==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} + + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@18.0.10: + resolution: {integrity: sha512-FJeH4bRpYoXiggcgriCGItKCSv3xkngJc4QCZ/rkQCogU3VYaLxYJoZl8Nw/b4+x7iij/pd+09mZ6A1dXzpL0A==} + engines: {node: '>= 20'} + hasBin: true + + markerjs2@2.32.7: + resolution: {integrity: sha512-HeFRZjmc43DOG3lSQp92z49cq2oCYpYn2pX++SkJAW1Dij4xJtRquVRf+cXeSZQWDX3ufns1Ry/bGk+zveP7rA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + pdfkit@0.19.1: + resolution: {integrity: sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==} + + pdfmake@0.3.11: + resolution: {integrity: sha512-Uc49J9hUMyuqJk+U+PxlpBpPr96A4HOOfesGx609EPr2ue82+5/Smq/KTAkEqh0/jUGSi1fumvqZ5yAWijJTJg==} + engines: {node: '>=20'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + png-js@1.1.0: + resolution: {integrity: sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==} + + polylabel@1.1.0: + resolution: {integrity: sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rollup@4.62.5: + resolution: {integrity: sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + seedrandom@3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sortablejs@1.15.7: + resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + svg-arc-to-cubic-bezier@3.2.0: + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + + timezone-groups@0.10.4: + resolution: {integrity: sha512-AnkJYrbb7uPkDCEqGeVJiawZNiwVlSkkeX4jZg1gTEguClhyX+/Ezn07KB6DT29tG3UN418ldmS/W6KqGOTDjg==} + engines: {node: '>=18.12.0'} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyqueue@2.0.3: + resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + xmldoc@2.0.3: + resolution: {integrity: sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w==} + engines: {node: '>=12.0.0'} + + xss@1.0.13: + resolution: {integrity: sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q==} + engines: {node: '>= 0.10.0'} + hasBin: true + +snapshots: + + '@amcharts/amcharts5@5.18.0': + dependencies: + '@types/d3': 7.4.3 + '@types/d3-chord': 3.0.6 + '@types/d3-hierarchy': 3.1.1 + '@types/d3-sankey': 0.11.2 + '@types/d3-shape': 3.1.8 + '@types/geojson': 7946.0.16 + '@types/polylabel': 1.1.3 + '@types/svg-arc-to-cubic-bezier': 3.2.3 + d3: 7.9.0 + d3-chord: 3.0.1 + d3-force: 3.0.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-sankey: 0.12.3 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-voronoi-treemap: 1.1.2 + flatpickr: 4.6.13 + markerjs2: 2.32.7 + pdfmake: 0.3.11 + polylabel: 1.1.0 + seedrandom: 3.0.5 + svg-arc-to-cubic-bezier: 3.2.0 + tslib: 2.8.1 + + '@arcgis/core@5.1.19': + dependencies: + '@amcharts/amcharts5': 5.18.0 + '@arcgis/toolkit': 5.1.19 + '@esri/arcgis-html-sanitizer': 4.1.0 + '@esri/calcite-components': 5.1.2 + '@vaadin/grid': 25.1.11 + '@zip.js/zip.js': 2.8.54 + luxon: 3.7.2 + marked: 18.0.10 + tslib: 2.8.1 + transitivePeerDependencies: + - '@lit/context' + + '@arcgis/lumina@5.1.19(lit@3.3.3)': + dependencies: + '@arcgis/toolkit': 5.1.19 + csstype: 3.2.3 + lit: 3.3.3 + tslib: 2.8.1 + + '@arcgis/toolkit@5.1.19': + dependencies: + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@esri/arcgis-html-sanitizer@4.1.0': + dependencies: + xss: 1.0.13 + + '@esri/calcite-components@5.1.2': + dependencies: + '@arcgis/lumina': 5.1.19(lit@3.3.3) + '@arcgis/toolkit': 5.1.19 + '@esri/calcite-ui-icons': 4.5.0 + '@floating-ui/dom': 1.8.0 + '@floating-ui/utils': 0.2.12 + '@types/sortablejs': 1.15.9 + color: 5.0.3 + composed-offset-position: 0.0.6(@floating-ui/utils@0.2.12) + es-toolkit: 1.51.0 + focus-trap: 8.2.2 + interactjs: 1.10.28 + lit: 3.3.3 + sortablejs: 1.15.7 + timezone-groups: 0.10.4 + type-fest: 4.41.0 + transitivePeerDependencies: + - '@lit/context' + + '@esri/calcite-ui-icons@4.5.0': {} + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@interactjs/types@1.10.28': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@lit-labs/ssr-dom-shim@1.6.0': {} + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/hashes@1.8.0': {} + + '@open-wc/dedupe-mixin@1.4.0': {} + + '@rollup/rollup-android-arm-eabi@4.62.5': + optional: true + + '@rollup/rollup-android-arm64@4.62.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.5': + optional: true + + '@rollup/rollup-darwin-x64@4.62.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.5': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.5': + optional: true + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@total-typescript/tsconfig@1.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.1': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.1': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@1.0.11': {} + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.4': {} + + '@types/d3-sankey@0.11.2': + dependencies: + '@types/d3-shape': 1.3.12 + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@1.3.12': + dependencies: + '@types/d3-path': 1.0.11 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.1 + '@types/d3-hierarchy': 3.1.1 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/polylabel@1.1.3': {} + + '@types/sortablejs@1.15.9': {} + + '@types/svg-arc-to-cubic-bezier@3.2.3': {} + + '@types/trusted-types@2.0.7': {} + + '@ugrc/tsconfigs@1.0.4': + dependencies: + '@total-typescript/tsconfig': 1.0.4 + + '@vaadin/a11y-base@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/component-base': 25.1.11 + lit: 3.3.3 + + '@vaadin/checkbox@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/a11y-base': 25.1.11 + '@vaadin/component-base': 25.1.11 + '@vaadin/field-base': 25.1.11 + '@vaadin/vaadin-themable-mixin': 25.1.11 + lit: 3.3.3 + + '@vaadin/component-base@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/vaadin-development-mode-detector': 2.0.7 + '@vaadin/vaadin-usage-statistics': 2.1.3 + lit: 3.3.3 + + '@vaadin/field-base@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/a11y-base': 25.1.11 + '@vaadin/component-base': 25.1.11 + lit: 3.3.3 + + '@vaadin/grid@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/a11y-base': 25.1.11 + '@vaadin/checkbox': 25.1.11 + '@vaadin/component-base': 25.1.11 + '@vaadin/lit-renderer': 25.1.11 + '@vaadin/text-field': 25.1.11 + '@vaadin/vaadin-themable-mixin': 25.1.11 + lit: 3.3.3 + + '@vaadin/input-container@25.1.11': + dependencies: + '@vaadin/component-base': 25.1.11 + '@vaadin/vaadin-themable-mixin': 25.1.11 + lit: 3.3.3 + + '@vaadin/lit-renderer@25.1.11': + dependencies: + lit: 3.3.3 + + '@vaadin/text-field@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/a11y-base': 25.1.11 + '@vaadin/component-base': 25.1.11 + '@vaadin/field-base': 25.1.11 + '@vaadin/input-container': 25.1.11 + '@vaadin/vaadin-themable-mixin': 25.1.11 + lit: 3.3.3 + + '@vaadin/vaadin-development-mode-detector@2.0.7': {} + + '@vaadin/vaadin-themable-mixin@25.1.11': + dependencies: + '@open-wc/dedupe-mixin': 1.4.0 + '@vaadin/component-base': 25.1.11 + lit: 3.3.3 + + '@vaadin/vaadin-usage-statistics@2.1.3': + dependencies: + '@vaadin/vaadin-development-mode-detector': 2.0.7 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@zip.js/zip.js@2.8.54': {} + + assertion-error@2.0.1: {} + + base64-js@0.0.8: {} + + base64-js@1.5.1: {} + + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + clone@2.1.2: {} + + color-convert@3.1.3: + dependencies: + color-name: 2.1.1 + + color-name@2.1.1: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.1 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + commander@2.20.3: {} + + commander@7.2.0: {} + + composed-offset-position@0.0.6(@floating-ui/utils@0.2.12): + dependencies: + '@floating-ui/utils': 0.2.12 + + cssfilter@0.0.10: {} + + csstype@3.2.3: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@2.0.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@2.0.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@2.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-voronoi-map@2.1.1: + dependencies: + d3-dispatch: 2.0.0 + d3-polygon: 2.0.0 + d3-timer: 2.0.0 + d3-weighted-voronoi: 1.1.3 + + d3-voronoi-treemap@1.1.2: + dependencies: + d3-voronoi-map: 2.1.1 + + d3-weighted-voronoi@1.1.3: + dependencies: + d3-array: 2.12.1 + d3-polygon: 2.0.0 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + dfa@1.2.0: {} + + es-module-lexer@1.7.0: {} + + es-toolkit@1.51.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + flatpickr@4.6.13: {} + + focus-trap@8.2.2: + dependencies: + tabbable: 6.5.0 + + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.23 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + + fsevents@2.3.3: + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + interactjs@1.10.28: + dependencies: + '@interactjs/types': 1.10.28 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + js-md5@0.8.3: {} + + js-tokens@9.0.1: {} + + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.6.0 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.3 + + lit-html@3.3.3: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.3: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.3 + + loupe@3.2.1: {} + + luxon@3.7.2: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@18.0.10: {} + + markerjs2@2.32.7: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + pako@0.2.9: {} + + pako@1.0.11: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + pdfkit@0.19.1: + dependencies: + '@noble/ciphers': 1.3.0 + '@noble/hashes': 1.8.0 + fontkit: 2.0.4 + js-md5: 0.8.3 + linebreak: 1.1.0 + png-js: 1.1.0 + + pdfmake@0.3.11: + dependencies: + linebreak: 1.1.0 + pdfkit: 0.19.1 + xmldoc: 2.0.3 + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + png-js@1.1.0: + dependencies: + browserify-zlib: 0.2.0 + + polylabel@1.1.0: + dependencies: + tinyqueue: 2.0.3 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + restructure@3.0.2: {} + + robust-predicates@3.0.3: {} + + rollup@4.62.5: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.5 + '@rollup/rollup-android-arm64': 4.62.5 + '@rollup/rollup-darwin-arm64': 4.62.5 + '@rollup/rollup-darwin-x64': 4.62.5 + '@rollup/rollup-freebsd-arm64': 4.62.5 + '@rollup/rollup-freebsd-x64': 4.62.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.5 + '@rollup/rollup-linux-arm-musleabihf': 4.62.5 + '@rollup/rollup-linux-arm64-gnu': 4.62.5 + '@rollup/rollup-linux-arm64-musl': 4.62.5 + '@rollup/rollup-linux-loong64-gnu': 4.62.5 + '@rollup/rollup-linux-loong64-musl': 4.62.5 + '@rollup/rollup-linux-ppc64-gnu': 4.62.5 + '@rollup/rollup-linux-ppc64-musl': 4.62.5 + '@rollup/rollup-linux-riscv64-gnu': 4.62.5 + '@rollup/rollup-linux-riscv64-musl': 4.62.5 + '@rollup/rollup-linux-s390x-gnu': 4.62.5 + '@rollup/rollup-linux-x64-gnu': 4.62.5 + '@rollup/rollup-linux-x64-musl': 4.62.5 + '@rollup/rollup-openbsd-x64': 4.62.5 + '@rollup/rollup-openharmony-arm64': 4.62.5 + '@rollup/rollup-win32-arm64-msvc': 4.62.5 + '@rollup/rollup-win32-ia32-msvc': 4.62.5 + '@rollup/rollup-win32-x64-gnu': 4.62.5 + '@rollup/rollup-win32-x64-msvc': 4.62.5 + fsevents: 2.3.3 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + sax@1.6.1: {} + + seedrandom@3.0.5: {} + + siginfo@2.0.0: {} + + sortablejs@1.15.7: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + svg-arc-to-cubic-bezier@3.2.0: {} + + tabbable@6.5.0: {} + + timezone-groups@0.10.4: {} + + tiny-inflate@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyqueue@2.0.3: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tslib@2.8.1: {} + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + + vite-node@3.2.4(@types/node@24.13.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@24.13.3): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.26 + rollup: 4.62.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + + vitest@3.2.7(@types/node@24.13.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.3) + vite-node: 3.2.4(@types/node@24.13.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + xmldoc@2.0.3: + dependencies: + sax: 1.6.1 + + xss@1.0.13: + dependencies: + commander: 2.20.3 + cssfilter: 0.0.10 diff --git a/attribute-rules/pnpm-workspace.yaml b/attribute-rules/pnpm-workspace.yaml new file mode 100644 index 0000000..1bc42ac --- /dev/null +++ b/attribute-rules/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +minimumReleaseAge: 10080 +minimumReleaseAgeStrict: true +minimumReleaseAgeExclude: + - '@ugrc/*' + +allowBuilds: + '@vaadin/vaadin-usage-statistics': true + esbuild: true diff --git a/attribute-rules/set_field_defaults.py b/attribute-rules/set_field_defaults.py new file mode 100644 index 0000000..40b5ffd --- /dev/null +++ b/attribute-rules/set_field_defaults.py @@ -0,0 +1,183 @@ +"""Assign field defaults programmatically based on ArcGIS field types. + +Example: + python attribute-rules/set_field_defaults.py \ + --in-table "Z:/data/County.gdb/Roads" \ + --dry-run +""" + +from __future__ import annotations + +import argparse + +import arcpy + + +TEXT_FIELD_TYPES = {"String"} +INTEGER_FIELD_TYPES = {"SmallInteger", "Integer", "BigInteger"} +SKIP_FIELD_TYPES = {"OID", "Geometry", "Blob", "Raster", "Date", "GUID", "GlobalID"} +SKIP_FIELD_NAMES = {"Shape_Length", "Shape_Area"} +FIELD_DEFAULT_OVERRIDES = {"CARTOCODE": "11"} + + +def _log(message: str) -> None: + print(message) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Assign default values to fields based on ArcGIS field types.", + epilog=( + "Notes:\n" + "- Required input: --in-table.\n" + "- Preview mode is the default; use --apply to write schema changes.\n" + "- Text fields default to an empty string; integer fields default to 0.\n" + "- Domain-controlled fields are only updated when that default is a valid coded value.\n" + "- CARTOCODE is overridden to default to 11." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + ) + required_args = parser.add_argument_group("required arguments") + optional_args = parser.add_argument_group("optional arguments") + + optional_args.add_argument( + "-h", + "--help", + action="help", + help="show this help message and exit", + ) + required_args.add_argument( + "--in-table", + required=True, + help="Target feature class or table whose field defaults will be assigned.", + ) + optional_args.add_argument( + "--apply", + action="store_true", + help="Write changes to the dataset. If omitted, only preview the planned changes.", + ) + optional_args.add_argument( + "--all-subtypes", + action="store_true", + help="Assign the default separately to every subtype code on the dataset.", + ) + optional_args.add_argument( + "--field", + action="append", + default=[], + help="Limit processing to one field name. Repeat the flag to target multiple fields.", + ) + + return parser.parse_args() + + +def _normalize_selected_fields(field_names: list[str]) -> set[str]: + return {name.upper() for name in field_names} + + +def _get_candidate_default(field: arcpy.Field) -> object | None: + override = FIELD_DEFAULT_OVERRIDES.get(field.name.upper()) + if override is not None: + return override + if field.type in TEXT_FIELD_TYPES: + return "" + if field.type in INTEGER_FIELD_TYPES: + return 0 + return None + + +def _get_domains(in_table: str) -> dict[str, object]: + workspace = arcpy.Describe(in_table).path + return {domain.name: domain for domain in arcpy.da.ListDomains(workspace)} + + +def _get_subtype_codes(in_table: str) -> list[int | str]: + subtypes = arcpy.da.ListSubtypes(in_table) + return [code for code in subtypes if code is not None] + + +def _field_is_selected(field_name: str, selected_fields: set[str]) -> bool: + if not selected_fields: + return True + return field_name.upper() in selected_fields + + +def _can_assign_domain_default(field: arcpy.Field, candidate: object, domains: dict[str, object]) -> bool: + if not field.domain: + return True + + domain = domains.get(field.domain) + if domain is None or domain.domainType != "CodedValue": + return False + + return candidate in domain.codedValues + + +def _assign_default( + args: argparse.Namespace, + field: arcpy.Field, + candidate: object, + subtype_codes: list[int | str], +) -> None: + if args.all_subtypes and subtype_codes: + for subtype_code in subtype_codes: + arcpy.management.AssignDefaultToField(args.in_table, field.name, candidate, subtype_code) + _log(f"SET subtype {subtype_code}: {field.name} = {candidate!r}") + return + + arcpy.management.AssignDefaultToField(args.in_table, field.name, candidate) + _log(f"SET: {field.name} = {candidate!r}") +def main() -> None: + args = _parse_args() + + if not arcpy.Exists(args.in_table): + raise RuntimeError(f"Target dataset does not exist: {args.in_table}") + + selected_fields = _normalize_selected_fields(args.field) + domains = _get_domains(args.in_table) + subtype_codes = _get_subtype_codes(args.in_table) if args.all_subtypes else [] + mode = "Apply" if args.apply else "Dry run" + + _log(f"{mode}: {'writing schema changes' if args.apply else 'no changes will be written'}") + _log(f"- in_table: {args.in_table}") + _log(f"- all_subtypes: {'yes' if args.all_subtypes else 'no'}") + _log(f"- selected_fields: {', '.join(args.field) if args.field else '(all eligible fields)'}") + + for field in arcpy.ListFields(args.in_table): + if field.required: + continue + if field.name in SKIP_FIELD_NAMES: + continue + if field.type in SKIP_FIELD_TYPES: + continue + if not _field_is_selected(field.name, selected_fields): + continue + + candidate = _get_candidate_default(field) + if candidate is None: + _log(f"SKIP unsupported type: {field.name} ({field.type})") + continue + + if not _can_assign_domain_default(field, candidate, domains): + if field.domain: + _log( + f"SKIP domain mismatch: {field.name} -> {candidate!r} is not a valid code in {field.domain}" + ) + else: + _log(f"SKIP unsupported domain type: {field.name} ({field.domain})") + continue + + if not args.apply: + if args.all_subtypes and subtype_codes: + for subtype_code in subtype_codes: + _log(f"WOULD SET subtype {subtype_code}: {field.name} = {candidate!r}") + else: + _log(f"WOULD SET: {field.name} = {candidate!r}") + continue + + _assign_default(args, field, candidate, subtype_codes) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/attribute-rules/test/_helpers.ts b/attribute-rules/test/_helpers.ts new file mode 100644 index 0000000..3b8b0b3 --- /dev/null +++ b/attribute-rules/test/_helpers.ts @@ -0,0 +1,22 @@ +import { readFile } from "node:fs/promises"; +import Graphic from "@arcgis/core/Graphic.js"; +import { createArcadeExecutor } from "@arcgis/core/arcade.js"; + +export async function loadExecutor(relativePath: string, variables: any[]) { + const expression = await readFile( + new URL(`../${relativePath}`, import.meta.url), + "utf8" + ); + + return createArcadeExecutor(expression, { variables }); +} + +export function feature(attributes: Record, geometry?: any) { + return new Graphic({ attributes, geometry }); +} + +export const lineGeometry = { + type: "polyline", + paths: [[[-111.9, 40.7], [-111.8, 40.7]]], + spatialReference: { wkid: 4326 } +}; \ No newline at end of file diff --git a/attribute-rules/test/constrain_to_attribute_domain.test.ts b/attribute-rules/test/constrain_to_attribute_domain.test.ts new file mode 100644 index 0000000..c050064 --- /dev/null +++ b/attribute-rules/test/constrain_to_attribute_domain.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "vitest"; +import FeatureLayer from "@arcgis/core/layers/FeatureLayer.js"; +import { feature, loadExecutor } from "./_helpers.js"; + +const executor = await loadExecutor( + "constraint/Roads_Edit/constrain_to_attribute_domain.arcade", + [{ name: "$feature", type: "feature" }] +); + +function roadFeature(attributes: Record) { + const layer = new FeatureLayer({ + title: "Roads", + fields: [ + { name: "OBJECTID", type: "oid" }, + { name: "SPEED_LMT", type: "integer", domain: { + type: "coded-value", name: "Speed limits", + codedValues: [{ code: 25, name: "25 mph" }, { code: 35, name: "35 mph" }] + } } + ], + objectIdField: "OBJECTID", + source: [] + }); + const road = feature({ + OBJECTID: 1, PREDIR: null, POSTTYPE: null, POSTDIR: null, AN_POSTDIR: null, + A1_PREDIR: null, A1_POSTTYPE: null, A1_POSTDIR: null, A2_PREDIR: null, + A2_POSTTYPE: null, A2_POSTDIR: null, VERT_LEVEL: null, ONEWAY: null, + SPEED_LMT: null, ...attributes + }); + (road as any).layer = layer; + return road; +} + +test("constrain_to_attribute_domain accepts a coded domain value", () => { + expect(executor.execute({ "$feature": roadFeature({ SPEED_LMT: 35 }) })).toBe(true); +}); + +test("constrain_to_attribute_domain rejects an invalid coded domain value", () => { + expect(executor.execute({ "$feature": roadFeature({ SPEED_LMT: 30 }) })).toEqual({ + errorMessage: "Invalid value [30] entered for field: SPEED_LMT" + }); +}); \ No newline at end of file diff --git a/attribute-rules/test/convert_to_null_if_no_value.test.ts b/attribute-rules/test/convert_to_null_if_no_value.test.ts new file mode 100644 index 0000000..cfecd60 --- /dev/null +++ b/attribute-rules/test/convert_to_null_if_no_value.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from "vitest"; +import { feature, loadExecutor } from "./_helpers.js"; + +const executor = await loadExecutor( + "calculation/Roads_Edit/convert_to_null_if_no_value.arcade", + [{ name: "$feature", type: "feature" }] +); + +test("convert_to_null_if_no_value converts blank text fields to null", () => { + const result = executor.execute({ + "$feature": feature({ + ADDRSYS_L: " ", ADDRSYS_R: "GRID", BIKE_L: "", BIKE_R: "Existing", + BIKE_PLN_L: null, BIKE_PLN_R: "Plan", DOT_HWYNAM: "", DOT_RTNAME: "Route", DOT_RTPART: " " + }) + }); + + expect(result).toEqual({ + result: { attributes: { + ADDRSYS_L: null, ADDRSYS_R: "GRID", BIKE_L: null, BIKE_R: "Existing", + BIKE_PLN_L: null, BIKE_PLN_R: "Plan", DOT_HWYNAM: null, DOT_RTNAME: "Route", DOT_RTPART: null + } } + }); +}); \ No newline at end of file diff --git a/attribute-rules/test/remove_apostrophes_from_name.test.ts b/attribute-rules/test/remove_apostrophes_from_name.test.ts new file mode 100644 index 0000000..52dc810 --- /dev/null +++ b/attribute-rules/test/remove_apostrophes_from_name.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "vitest"; +import { feature, loadExecutor } from "./_helpers.js"; + +const executor = await loadExecutor( + "calculation/Roads_Edit/remove_apostrophes_from_name.arcade", + [{ name: "$feature", type: "feature" }] +); + +test("removes apostrophes from NAME", () => { + expect(executor.execute({ "$feature": feature({ NAME: "O'CONNELL" }) })).toBe("OCONNELL"); +}); + +test("leaves names without apostrophes unchanged", () => { + expect(executor.execute({ "$feature": feature({ NAME: "WASHINGTON" }) })).toBe("WASHINGTON"); +}); \ No newline at end of file diff --git a/attribute-rules/test/spatial_calculations.test.ts b/attribute-rules/test/spatial_calculations.test.ts new file mode 100644 index 0000000..4741144 --- /dev/null +++ b/attribute-rules/test/spatial_calculations.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from "vitest"; +import ArcGISMap from "@arcgis/core/Map.js"; +import FeatureLayer from "@arcgis/core/layers/FeatureLayer.js"; +import { feature, lineGeometry, loadExecutor } from "./_helpers.js"; + +function emptyPolygonLayer(title: string, fields: any[]) { + return new FeatureLayer({ + title, geometryType: "polygon", spatialReference: { wkid: 4326 }, + objectIdField: "OBJECTID", fields: [{ name: "OBJECTID", type: "oid" }, ...fields], source: [] + }); +} + +function emptyDatastore() { + return new ArcGISMap({ layers: [ + emptyPolygonLayer("AddressSystemQuadrants", [{ name: "QUADRANT", type: "string" }, { name: "GRID_NAME", type: "string" }]), + emptyPolygonLayer("Counties", [{ name: "FIPS_STR", type: "string" }, { name: "NAME", type: "string" }]), + emptyPolygonLayer("ZipCodes", [{ name: "ZIP5", type: "string" }, { name: "NAME", type: "string" }]), + emptyPolygonLayer("Municipalities", [{ name: "NAME", type: "string" }]) + ] }); +} + +const spatialExecutor = await loadExecutor("calculation/Roads_Edit/spatial_assignments.arcade", [ + { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } +]); +const ownerExecutor = await loadExecutor("calculation/Roads_Edit/calculate_dot_owner.arcade", [ + { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } +]); +const uniqueIdExecutor = await loadExecutor("calculation/Roads_Edit/unique_id.arcade", [ + { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } +]); + +test("spatial_assignments compiles with its datastore contract", () => { + expect(spatialExecutor).toBeDefined(); +}); + +test("calculate_dot_owner assigns UDOT to both sides for zero-prefixed routes", async () => { + const result = await ownerExecutor.executeAsync({ + "$feature": feature({ DOT_RTNAME: "0123" }, lineGeometry), "$datastore": emptyDatastore() + }); + expect(result).toEqual({ result: { attributes: { DOT_OWN_L: "UDOT", DOT_OWN_R: "UDOT" } } }); +}); + +test("unique_id reports missing geometry", async () => { + const result = await uniqueIdExecutor.executeAsync({ + "$feature": feature({ NAME: "MAIN", POSTDIR: null, POSTTYPE: null }), "$datastore": emptyDatastore() + }); + expect(result).toBe("ERR:MISSING_GEOMETRY"); +}); \ No newline at end of file diff --git a/attribute-rules/test/text_calculations.test.ts b/attribute-rules/test/text_calculations.test.ts new file mode 100644 index 0000000..99b649c --- /dev/null +++ b/attribute-rules/test/text_calculations.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "vitest"; +import { feature, loadExecutor } from "./_helpers.js"; + +const whitespaceExecutor = await loadExecutor( + "calculation/Roads_Edit/remove_extra_whitespace.arcade", + [{ name: "$feature", type: "feature" }] +); +const fullnameExecutor = await loadExecutor( + "calculation/Roads_Edit/fullname_calculation.arcade", + [{ name: "$feature", type: "feature" }] +); + +test("remove_extra_whitespace trims and collapses spaces", () => { + expect(whitespaceExecutor.execute({ "$feature": feature({ NAME: " NORTH MAIN " }) })).toBe("NORTH MAIN"); +}); + +test("remove_extra_whitespace returns an empty string for a missing NAME", () => { + expect(whitespaceExecutor.execute({ "$feature": feature({ NAME: null }) })).toBe(""); +}); + +test("fullname_calculation combines normalized name parts", () => { + expect(fullnameExecutor.execute({ + "$feature": feature({ NAME: " O'MAIN ", POSTDIR: null, POSTTYPE: " RD " }) + })).toBe("OMAIN RD"); +}); + +test("fullname_calculation omits missing name parts", () => { + expect(fullnameExecutor.execute({ + "$feature": feature({ NAME: null, POSTDIR: " N ", POSTTYPE: null }) + })).toBe("N"); +}); \ No newline at end of file diff --git a/attribute-rules/tsconfig.json b/attribute-rules/tsconfig.json new file mode 100644 index 0000000..d70246d --- /dev/null +++ b/attribute-rules/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@ugrc/tsconfigs/vite-config", + "include": ["test/**/*.ts"] +} \ No newline at end of file diff --git a/attribute-rules/vitest.config.ts b/attribute-rules/vitest.config.ts new file mode 100644 index 0000000..09720e7 --- /dev/null +++ b/attribute-rules/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"] + } +}); \ No newline at end of file From cc93bd5d72492d5ed8a8180fc0dfe424a8ded9ef Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 13:11:52 -0600 Subject: [PATCH 02/11] chore: remove unnecessary prompt engineering comment for dot ownership --- .../calculation/Roads_Edit/calculate_dot_owner.arcade | 9 --------- 1 file changed, 9 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade index c0629f6..ac04e69 100644 --- a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade +++ b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade @@ -58,15 +58,6 @@ function midpointAlongLine(geo) { } var dotRtName = Trim(IIf(IsEmpty($feature.DOT_RTNAME), '', Text($feature.DOT_RTNAME))); -/* -- If DOT_RTNAME starts with 0, it’s a UDOT street: DOT_OWN is just UDOT -- Else - - If the left muni exists, set it to MU + β€œ β€œ + NAME attribute of the left muni - - how is left muni found? - - Else, if the left county exists, set it to the county’s FIPS_STR - - Else, just set it to blank. -- This is the same for both DOT_OWN_L and DOT_OWN_R -*/ var dotOwnL = ''; var dotOwnR = ''; if (Left(dotRtName, 1) == '0') { From dc2dee531dd4e5c94bca88255baf1ccb857bcb58 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 13:16:45 -0600 Subject: [PATCH 03/11] chore: apply labelPoint to Centroid call Co-authored-by: Scott Davis --- .../calculation/Roads_Edit/spatial_assignments.arcade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade index 032f0fe..a4f7443 100644 --- a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade +++ b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade @@ -7,8 +7,8 @@ var countyLayer = FeatureSetByName($datastore, "Counties", ["FIPS_STR"]); var zipCodeLayer = FeatureSetByName($datastore, "ZipCodes", ["ZIP5", "NAME"]); var municipalityLayer = FeatureSetByName($datastore, "Municipalities", ["NAME"]); -var roadLeft = Centroid(Offset($feature, -15, 'meters', 'square')); -var roadRight = Centroid(Offset($feature, 15, 'meters', 'square')); +var roadLeft = Centroid(Offset($feature, -15, 'meters', 'square'), 'labelPoint'); +var roadRight = Centroid(Offset($feature, 15, 'meters', 'square'), 'labelPoint'); function fieldOrNull(feature, fieldName) { if (IsEmpty(feature)) { From 8d824aaf15f43661cebef2245b5c19c4b37afb02 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 13:33:41 -0600 Subject: [PATCH 04/11] refactor: no longer need new_ar.py --- README.md | 7 +++---- attribute-rules/new_ar.py | 21 --------------------- 2 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 attribute-rules/new_ar.py diff --git a/README.md b/README.md index fc57636..03eaed8 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,12 @@ Tools for working with UTRANS data ## Attribute Rule Scripts -The attribute-rule utilities are in [attribute-rules/new_ar.py](attribute-rules/new_ar.py), [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py), and [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py). +The attribute-rule utilities are in [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py) and [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py). - [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py) is the primary script. - Default mode is idempotent deploy: add if missing, alter if mutable changes, recreate when immutable changes require it. - Use `--mode add` for strict add-only behavior. - Use `--mode alter` to update an existing rule only. -- [attribute-rules/new_ar.py](attribute-rules/new_ar.py) is a compatibility wrapper that forwards to deploy script add mode. - [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py) recursively deploys every `.arcade` file using idempotent deploy mode. It infers the rule type from the top-level folder (`calculation`, `constraint`, or `validation`) and reads optional `// KEY: value` metadata from each script. Supported per-script metadata keys are `RULE_NAME`, `FIELD`, `EVENTS`, `IS_EDITABLE`, `ERROR_NUMBER`, `ERROR_MESSAGE`, `DESCRIPTION`, and `SUBTYPE`. Use `FIELD:` with no value for calculation rules that return an `attributes` dictionary for multiple fields. @@ -21,8 +20,8 @@ Examples: # deploy mode (default) python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --field FULLNAME -# add-only compatibility behavior -python attribute-rules/new_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --field FULLNAME +# add-only behavior +python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode add --field FULLNAME # alter existing rule only python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode alter --field FULLNAME diff --git a/attribute-rules/new_ar.py b/attribute-rules/new_ar.py deleted file mode 100644 index 2697031..0000000 --- a/attribute-rules/new_ar.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Compatibility wrapper for add-only attribute rule behavior. - -For new deployments, use deploy_ar.py directly. -""" - -from __future__ import annotations - -import sys - -from deploy_ar import main as deploy_main - - -def main() -> None: - argv = sys.argv[1:] - if "--mode" in argv: - raise RuntimeError("new_ar.py does not accept --mode. Use deploy_ar.py for mode selection.") - deploy_main(["--mode", "add", *argv]) - - -if __name__ == "__main__": - main() From 3c339cb6aa90b3d3a5ef954543238e18fe6e5baf Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 14:25:18 -0600 Subject: [PATCH 05/11] refactor: moving county computation to within conditional to save on compute time --- .../Roads_Edit/calculate_dot_owner.arcade | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade index ac04e69..e1981d1 100644 --- a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade +++ b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade @@ -80,26 +80,30 @@ if (Left(dotRtName, 1) == '0') { // 4. Query the Counties polygon FeatureSet var countyFS = FeatureSetByName($datastore, "Counties", ["NAME"], true); - var leftCounty = First(Intersects(countyFS, leftPt)); - var rightCounty = First(Intersects(countyFS, rightPt)); // 5. Determine DOT_OWN_L if (!IsEmpty(leftMuni)) { dotOwnL = 'MU ' + leftMuni.NAME; - } else if (!IsEmpty(leftCounty)) { - // Convert NAME as such: KANE -> Kane - dotOwnL = 'CO ' + Proper(leftCounty.NAME); } else { - dotOwnL = ''; + var leftCounty = First(Intersects(countyFS, leftPt)); + if (!IsEmpty(leftCounty)) { + // Convert NAME as such: KANE -> Kane + dotOwnL = 'CO ' + Proper(leftCounty.NAME); + } else { + dotOwnL = ''; + } } // 6. Determine DOT_OWN_R if (!IsEmpty(rightMuni)) { dotOwnR = 'MU ' + rightMuni.NAME; - } else if (!IsEmpty(rightCounty)) { - dotOwnR = 'CO ' + Proper(rightCounty.NAME); } else { - dotOwnR = ''; + var rightCounty = First(Intersects(countyFS, rightPt)); + if (!IsEmpty(rightCounty)) { + dotOwnR = 'CO ' + Proper(rightCounty.NAME); + } else { + dotOwnR = ''; + } } } From 4ca9a5eb5e8f797283c4bf232c652b738b1e0d12 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 15:31:34 -0600 Subject: [PATCH 06/11] refactor: formatting --- .vscode/settings.json | 5 + attribute-rules/deploy_all_ar.py | 212 +++++++++++++++---------------- 2 files changed, 111 insertions(+), 106 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e57f5b..9458119 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -79,5 +79,10 @@ "[xaml]": { "editor.formatOnSave": false }, + "[python]": { + "editor.insertSpaces": true, + "editor.tabSize": 4, + "editor.detectIndentation": false + }, "editor.formatOnSave": true } diff --git a/attribute-rules/deploy_all_ar.py b/attribute-rules/deploy_all_ar.py index 3160b26..9f93ecb 100644 --- a/attribute-rules/deploy_all_ar.py +++ b/attribute-rules/deploy_all_ar.py @@ -18,141 +18,141 @@ TYPE_BY_FOLDER = { - "calculation": "CALCULATION", - "constraint": "CONSTRAINT", - "validation": "VALIDATION", + "calculation": "CALCULATION", + "constraint": "CONSTRAINT", + "validation": "VALIDATION", } METADATA_PATTERN = re.compile(r"^\s*//\s*([A-Z][A-Z0-9_ ]*)\s*:\s*(.*?)\s*$") @dataclass(frozen=True) class RuleDeployment: - arcade_path: Path - rule_type: str - metadata: dict[str, str] + arcade_path: Path + rule_type: str + metadata: dict[str, str] def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Idempotently deploy every .arcade attribute rule under attribute-rules.", - ) - parser.add_argument( - "--in-table", - required=True, - help="Target feature class or table where all rules will be managed.", - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Preview each deploy action without writing changes.", - ) - parser.add_argument( - "--no-recreate", - action="store_true", - help="Fail rather than delete and recreate a rule with immutable differences.", - ) - return parser.parse_args(argv) + parser = argparse.ArgumentParser( + description="Idempotently deploy every .arcade attribute rule under attribute-rules.", + ) + parser.add_argument( + "--in-table", + required=True, + help="Target feature class or table where all rules will be managed.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview each deploy action without writing changes.", + ) + parser.add_argument( + "--no-recreate", + action="store_true", + help="Fail rather than delete and recreate a rule with immutable differences.", + ) + return parser.parse_args(argv) def _read_metadata(arcade_path: Path) -> dict[str, str]: - metadata: dict[str, str] = {} - for line in arcade_path.read_text(encoding="utf-8").splitlines(): - match = METADATA_PATTERN.match(line) - if match is None: - continue - key = re.sub(r"\s+", "_", match.group(1).strip()) - metadata[key] = match.group(2).strip() - return metadata + metadata: dict[str, str] = {} + for line in arcade_path.read_text(encoding="utf-8").splitlines(): + match = METADATA_PATTERN.match(line) + if match is None: + continue + key = re.sub(r"\s+", "_", match.group(1).strip()) + metadata[key] = match.group(2).strip() + return metadata def _rule_type_for_path(arcade_path: Path) -> str: - try: - category = arcade_path.relative_to(ATTRIBUTE_RULES_ROOT).parts[0].casefold() - except (ValueError, IndexError) as error: - raise ValueError(f"Cannot determine rule type for {arcade_path}") from error + try: + category = arcade_path.relative_to(ATTRIBUTE_RULES_ROOT).parts[0].casefold() + except (ValueError, IndexError) as error: + raise ValueError(f"Cannot determine rule type for {arcade_path}") from error - try: - return TYPE_BY_FOLDER[category] - except KeyError as error: - allowed = ", ".join(sorted(TYPE_BY_FOLDER)) - raise ValueError( - f"{arcade_path} must be below one of these folders: {allowed}" - ) from error + try: + return TYPE_BY_FOLDER[category] + except KeyError as error: + allowed = ", ".join(sorted(TYPE_BY_FOLDER)) + raise ValueError( + f"{arcade_path} must be below one of these folders: {allowed}" + ) from error def _discover_rules() -> list[RuleDeployment]: - rules: list[RuleDeployment] = [] - for arcade_path in sorted(ATTRIBUTE_RULES_ROOT.rglob("*.arcade")): - rules.append( - RuleDeployment( - arcade_path=arcade_path, - rule_type=_rule_type_for_path(arcade_path), - metadata=_read_metadata(arcade_path), - ) - ) - if not rules: - raise RuntimeError(f"No .arcade scripts found under {ATTRIBUTE_RULES_ROOT}") - return rules + rules: list[RuleDeployment] = [] + for arcade_path in sorted(ATTRIBUTE_RULES_ROOT.rglob("*.arcade")): + rules.append( + RuleDeployment( + arcade_path=arcade_path, + rule_type=_rule_type_for_path(arcade_path), + metadata=_read_metadata(arcade_path), + ) + ) + if not rules: + raise RuntimeError(f"No .arcade scripts found under {ATTRIBUTE_RULES_ROOT}") + return rules def _normalize_events(events: str) -> str: - return ";".join(part.strip().upper() for part in re.split(r"[;,]", events) if part.strip()) + return ";".join(part.strip().upper() for part in re.split(r"[;,]", events) if part.strip()) def _deployment_argv(deployment: RuleDeployment, args: argparse.Namespace) -> list[str]: - metadata = deployment.metadata - folder = deployment.arcade_path.parent.relative_to(ATTRIBUTE_RULES_ROOT).as_posix() - argv = [ - deployment.arcade_path.name, - "--in-table", - args.in_table, - "--mode", - "deploy", - "--folder", - folder, - "--type", - metadata.get("TYPE", deployment.rule_type).upper(), - ] - - if metadata.get("TYPE", deployment.rule_type).upper() != deployment.rule_type: - raise ValueError( - f"{deployment.arcade_path}: TYPE must match its {deployment.rule_type.lower()} folder" - ) - - for metadata_key, option in ( - ("RULE_NAME", "--rule-name"), - ("FIELD", "--field"), - ("EVENTS", "--triggering-events"), - ("IS_EDITABLE", "--is-editable"), - ("ERROR_NUMBER", "--error-number"), - ("ERROR_MESSAGE", "--error-message"), - ("DESCRIPTION", "--description"), - ("SUBTYPE", "--subtype"), - ): - if metadata_key in metadata: - value = metadata[metadata_key] - if metadata_key == "EVENTS": - value = _normalize_events(value) - argv.extend([option, value]) - - if args.dry_run: - argv.append("--dry-run") - if args.no_recreate: - argv.append("--no-recreate") - return argv + metadata = deployment.metadata + folder = deployment.arcade_path.parent.relative_to(ATTRIBUTE_RULES_ROOT).as_posix() + argv = [ + deployment.arcade_path.name, + "--in-table", + args.in_table, + "--mode", + "deploy", + "--folder", + folder, + "--type", + metadata.get("TYPE", deployment.rule_type).upper(), + ] + + if metadata.get("TYPE", deployment.rule_type).upper() != deployment.rule_type: + raise ValueError( + f"{deployment.arcade_path}: TYPE must match its {deployment.rule_type.lower()} folder" + ) + + for metadata_key, option in ( + ("RULE_NAME", "--rule-name"), + ("FIELD", "--field"), + ("EVENTS", "--triggering-events"), + ("IS_EDITABLE", "--is-editable"), + ("ERROR_NUMBER", "--error-number"), + ("ERROR_MESSAGE", "--error-message"), + ("DESCRIPTION", "--description"), + ("SUBTYPE", "--subtype"), + ): + if metadata_key in metadata: + value = metadata[metadata_key] + if metadata_key == "EVENTS": + value = _normalize_events(value) + argv.extend([option, value]) + + if args.dry_run: + argv.append("--dry-run") + if args.no_recreate: + argv.append("--no-recreate") + return argv def main(argv: list[str] | None = None) -> None: - args = _parse_args(argv) - rules = _discover_rules() + args = _parse_args(argv) + rules = _discover_rules() - # Build every command before changing the target so invalid metadata fails cleanly. - deployments = [(rule, _deployment_argv(rule, args)) for rule in rules] - print(f"Deploying {len(deployments)} attribute rule(s) to: {args.in_table}", flush=True) - for deployment, deploy_argv in deployments: - print(f"\n=== {deployment.arcade_path.relative_to(ATTRIBUTE_RULES_ROOT)} ===", flush=True) - deploy_rule(deploy_argv) + # Build every command before changing the target so invalid metadata fails cleanly. + deployments = [(rule, _deployment_argv(rule, args)) for rule in rules] + print(f"Deploying {len(deployments)} attribute rule(s) to: {args.in_table}", flush=True) + for deployment, deploy_argv in deployments: + print(f"\n=== {deployment.arcade_path.relative_to(ATTRIBUTE_RULES_ROOT)} ===", flush=True) + deploy_rule(deploy_argv) if __name__ == "__main__": - main() \ No newline at end of file + main() \ No newline at end of file From 94c1324b68bf0e0b626b526c53d6739e16d98f31 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 15:59:49 -0600 Subject: [PATCH 07/11] refactor: fix arcade indent level --- .vscode/settings.json | 5 + .../Roads_Edit/calculate_dot_owner.arcade | 190 +++++++++--------- .../convert_to_null_if_no_value.arcade | 24 +-- .../Roads_Edit/fullname_calculation.arcade | 16 +- .../Roads_Edit/remove_extra_whitespace.arcade | 2 +- .../Roads_Edit/spatial_assignments.arcade | 40 ++-- .../calculation/Roads_Edit/unique_id.arcade | 144 ++++++------- .../constrain_to_attribute_domain.arcade | 70 +++---- 8 files changed, 248 insertions(+), 243 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9458119..f52c7c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -84,5 +84,10 @@ "editor.tabSize": 4, "editor.detectIndentation": false }, + "[javascript]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.detectIndentation": false + }, "editor.formatOnSave": true } diff --git a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade index e1981d1..a554d24 100644 --- a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade +++ b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade @@ -4,114 +4,114 @@ Expects($feature, 'DOT_OWN_L', 'DOT_OWN_R', 'DOT_RTNAME'); function distance2d(from, to) { - var dx = to.x - from.x; - var dy = to.y - from.y; + var dx = to.x - from.x; + var dy = to.y - from.y; - return Sqrt(dx * dx + dy * dy); + return Sqrt(dx * dx + dy * dy); } // Arcade has no linear referencing, so walk the vertices to find the point halfway along the line. function midpointAlongLine(geo) { - var paths = geo.paths; - if (IsEmpty(paths) || Count(paths) == 0) { - return null; - } - - var total = 0; - for (var p in paths) { - var path = paths[p]; - for (var v = 1; v < Count(path); v++) { - total += distance2d(path[v - 1], path[v]); - } - } - - var firstInPaths = paths[0][0]; - if (total == 0) { - return { 'x': firstInPaths.x, 'y': firstInPaths.y }; - } - - var target = total / 2; - var traveled = 0; - for (var i in paths) { - var current = paths[i]; - for (var j = 1; j < Count(current); j++) { - var start = current[j - 1]; - var end = current[j]; - var len = distance2d(start, end); - - if (len > 0) { - if (traveled + len >= target) { - var ratio = (target - traveled) / len; - - return { - 'x': start.x + (end.x - start.x) * ratio, - 'y': start.y + (end.y - start.y) * ratio - }; - } - - traveled += len; - } - } - } - - return { 'x': firstInPaths.x, 'y': firstInPaths.y }; + var paths = geo.paths; + if (IsEmpty(paths) || Count(paths) == 0) { + return null; + } + + var total = 0; + for (var p in paths) { + var path = paths[p]; + for (var v = 1; v < Count(path); v++) { + total += distance2d(path[v - 1], path[v]); + } + } + + var firstInPaths = paths[0][0]; + if (total == 0) { + return { 'x': firstInPaths.x, 'y': firstInPaths.y }; + } + + var target = total / 2; + var traveled = 0; + for (var i in paths) { + var current = paths[i]; + for (var j = 1; j < Count(current); j++) { + var start = current[j - 1]; + var end = current[j]; + var len = distance2d(start, end); + + if (len > 0) { + if (traveled + len >= target) { + var ratio = (target - traveled) / len; + + return { + 'x': start.x + (end.x - start.x) * ratio, + 'y': start.y + (end.y - start.y) * ratio + }; + } + + traveled += len; + } + } + } + + return { 'x': firstInPaths.x, 'y': firstInPaths.y }; } var dotRtName = Trim(IIf(IsEmpty($feature.DOT_RTNAME), '', Text($feature.DOT_RTNAME))); var dotOwnL = ''; var dotOwnR = ''; if (Left(dotRtName, 1) == '0') { - dotOwnL = 'UDOT'; - dotOwnR = 'UDOT'; + dotOwnL = 'UDOT'; + dotOwnR = 'UDOT'; } else { - // 1. Get the line geometry and calculate the midpoint (50% along line) - var lineGeom = Geometry($feature); - var midPt = midpointAlongLine(lineGeom); // use the custom midpoint function - - // 2. Create left (-15m) and right (+15m) offset points - // Note: Offset syntax depends on unit settings, but conceptually: - var leftPt = Point({ 'x': midPt.x - 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); - var rightPt = Point({ 'x': midPt.x + 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); - - // 3. Query the Municipalities polygon FeatureSet - var muniFS = FeatureSetByName($datastore, "Municipalities", ["NAME"], true); - var leftMuni = First(Intersects(muniFS, leftPt)); - var rightMuni = First(Intersects(muniFS, rightPt)); - - // 4. Query the Counties polygon FeatureSet - var countyFS = FeatureSetByName($datastore, "Counties", ["NAME"], true); - - // 5. Determine DOT_OWN_L - if (!IsEmpty(leftMuni)) { - dotOwnL = 'MU ' + leftMuni.NAME; - } else { - var leftCounty = First(Intersects(countyFS, leftPt)); - if (!IsEmpty(leftCounty)) { - // Convert NAME as such: KANE -> Kane - dotOwnL = 'CO ' + Proper(leftCounty.NAME); - } else { - dotOwnL = ''; - } - } - - // 6. Determine DOT_OWN_R - if (!IsEmpty(rightMuni)) { - dotOwnR = 'MU ' + rightMuni.NAME; - } else { - var rightCounty = First(Intersects(countyFS, rightPt)); - if (!IsEmpty(rightCounty)) { - dotOwnR = 'CO ' + Proper(rightCounty.NAME); - } else { - dotOwnR = ''; - } - } + // 1. Get the line geometry and calculate the midpoint (50% along line) + var lineGeom = Geometry($feature); + var midPt = midpointAlongLine(lineGeom); // use the custom midpoint function + + // 2. Create left (-15m) and right (+15m) offset points + // Note: Offset syntax depends on unit settings, but conceptually: + var leftPt = Point({ 'x': midPt.x - 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); + var rightPt = Point({ 'x': midPt.x + 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); + + // 3. Query the Municipalities polygon FeatureSet + var muniFS = FeatureSetByName($datastore, "Municipalities", ["NAME"], true); + var leftMuni = First(Intersects(muniFS, leftPt)); + var rightMuni = First(Intersects(muniFS, rightPt)); + + // 4. Query the Counties polygon FeatureSet + var countyFS = FeatureSetByName($datastore, "Counties", ["NAME"], true); + + // 5. Determine DOT_OWN_L + if (!IsEmpty(leftMuni)) { + dotOwnL = 'MU ' + leftMuni.NAME; + } else { + var leftCounty = First(Intersects(countyFS, leftPt)); + if (!IsEmpty(leftCounty)) { + // Convert NAME as such: KANE -> Kane + dotOwnL = 'CO ' + Proper(leftCounty.NAME); + } else { + dotOwnL = ''; + } + } + + // 6. Determine DOT_OWN_R + if (!IsEmpty(rightMuni)) { + dotOwnR = 'MU ' + rightMuni.NAME; + } else { + var rightCounty = First(Intersects(countyFS, rightPt)); + if (!IsEmpty(rightCounty)) { + dotOwnR = 'CO ' + Proper(rightCounty.NAME); + } else { + dotOwnR = ''; + } + } } return { - "result": { - "attributes": { - "DOT_OWN_L": dotOwnL, - "DOT_OWN_R": dotOwnR - } - } + "result": { + "attributes": { + "DOT_OWN_L": dotOwnL, + "DOT_OWN_R": dotOwnR + } + } }; \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade b/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade index b39c211..f254ca8 100644 --- a/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade +++ b/attribute-rules/calculation/Roads_Edit/convert_to_null_if_no_value.arcade @@ -7,20 +7,20 @@ Expects($feature, 'ADDRSYS_L', 'ADDRSYS_R', 'BIKE_L', 'BIKE_R', 'BIKE_PLN_L', 'B var updates = {}; for (var i in textFields) { - var f = textFields[i]; - var val = $feature[f]; - - // Convert empty/whitespace strings to null, leave valid values alone - if (IsEmpty(Trim(val))) { - updates[f] = null; - } else { - updates[f] = val; - } + var f = textFields[i]; + var val = $feature[f]; + + // Convert empty/whitespace strings to null, leave valid values alone + if (IsEmpty(Trim(val))) { + updates[f] = null; + } else { + updates[f] = val; + } } // For calculation rules, "attributes" must be nested under "result". return { - "result": { - "attributes": updates - } + "result": { + "attributes": updates + } }; diff --git a/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade b/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade index d31f80d..0bf3bf0 100644 --- a/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade +++ b/attribute-rules/calculation/Roads_Edit/fullname_calculation.arcade @@ -2,11 +2,11 @@ // FIELD: FULLNAME // EVENTS: INSERT, UPDATE function normalizePart(value) { - var part = Trim(IIf(IsEmpty(value), "", Text(value))); - while (Find(" ", part) > -1) { - part = Replace(part, " ", " "); - } - return part; + var part = Trim(IIf(IsEmpty(value), "", Text(value))); + while (Find(" ", part) > -1) { + part = Replace(part, " ", " "); + } + return part; } var name = normalizePart(Replace(IIf(IsEmpty($feature.NAME), "", $feature.NAME), "'", "")); @@ -15,13 +15,13 @@ var posttype = normalizePart($feature.POSTTYPE); var parts = []; if (!IsEmpty(name)) { - Push(parts, name); + Push(parts, name); } if (!IsEmpty(postdir)) { - Push(parts, postdir); + Push(parts, postdir); } if (!IsEmpty(posttype)) { - Push(parts, posttype); + Push(parts, posttype); } // Test case: NAME=" MAIN ", POSTDIR=null, POSTTYPE=" RD " -> "MAIN RD" diff --git a/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade b/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade index 6a32e99..dbc1d5f 100644 --- a/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade +++ b/attribute-rules/calculation/Roads_Edit/remove_extra_whitespace.arcade @@ -6,7 +6,7 @@ var value = Trim(IIf(IsEmpty($feature.NAME), "", Text($feature.NAME))); while (Find(" ", value) > -1) { - value = Replace(value, " ", " "); + value = Replace(value, " ", " "); } return value; diff --git a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade index a4f7443..98c57db 100644 --- a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade +++ b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade @@ -11,11 +11,11 @@ var roadLeft = Centroid(Offset($feature, -15, 'meters', 'square'), 'labelPoint') var roadRight = Centroid(Offset($feature, 15, 'meters', 'square'), 'labelPoint'); function fieldOrNull(feature, fieldName) { - if (IsEmpty(feature)) { - return null; - } + if (IsEmpty(feature)) { + return null; + } - return feature[fieldName]; + return feature[fieldName]; } var quadrantLeftFeature = First(Intersects(addressSystemQuadrantsLayer, roadLeft)); @@ -28,20 +28,20 @@ var municipalityLeftFeature = First(Intersects(municipalityLayer, roadLeft)); var municipalityRightFeature = First(Intersects(municipalityLayer, roadRight)); return { - "result": { - "attributes": { - "QUADRANT_L": fieldOrNull(quadrantLeftFeature, "QUADRANT"), - "QUADRANT_R": fieldOrNull(quadrantRightFeature, "QUADRANT"), - "COUNTY_L": fieldOrNull(countyLeftFeature, "FIPS_STR"), - "COUNTY_R": fieldOrNull(countyRightFeature, "FIPS_STR"), - "ADDRSYS_L": fieldOrNull(quadrantLeftFeature, "GRID_NAME"), - "ADDRSYS_R": fieldOrNull(quadrantRightFeature, "GRID_NAME"), - "POSTCOMM_L": fieldOrNull(zipCodeLeftFeature, "NAME"), - "POSTCOMM_R": fieldOrNull(zipCodeRightFeature, "NAME"), - "ZIPCODE_L": fieldOrNull(zipCodeLeftFeature, "ZIP5"), - "ZIPCODE_R": fieldOrNull(zipCodeRightFeature, "ZIP5"), - "INCMUNI_L": fieldOrNull(municipalityLeftFeature, "NAME"), - "INCMUNI_R": fieldOrNull(municipalityRightFeature, "NAME") - } - } + "result": { + "attributes": { + "QUADRANT_L": fieldOrNull(quadrantLeftFeature, "QUADRANT"), + "QUADRANT_R": fieldOrNull(quadrantRightFeature, "QUADRANT"), + "COUNTY_L": fieldOrNull(countyLeftFeature, "FIPS_STR"), + "COUNTY_R": fieldOrNull(countyRightFeature, "FIPS_STR"), + "ADDRSYS_L": fieldOrNull(quadrantLeftFeature, "GRID_NAME"), + "ADDRSYS_R": fieldOrNull(quadrantRightFeature, "GRID_NAME"), + "POSTCOMM_L": fieldOrNull(zipCodeLeftFeature, "NAME"), + "POSTCOMM_R": fieldOrNull(zipCodeRightFeature, "NAME"), + "ZIPCODE_L": fieldOrNull(zipCodeLeftFeature, "ZIP5"), + "ZIPCODE_R": fieldOrNull(zipCodeRightFeature, "ZIP5"), + "INCMUNI_L": fieldOrNull(municipalityLeftFeature, "NAME"), + "INCMUNI_R": fieldOrNull(municipalityRightFeature, "NAME") + } + } } diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade index 505a61b..d3dc942 100644 --- a/attribute-rules/calculation/Roads_Edit/unique_id.arcade +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -8,115 +8,115 @@ Expects($feature, 'NAME', 'POSTDIR', 'POSTTYPE', 'UNIQUE_ID'); var DIGITS = '0123456789'; function distance2d(from, to) { - var dx = to.x - from.x; - var dy = to.y - from.y; + var dx = to.x - from.x; + var dy = to.y - from.y; - return Sqrt(dx * dx + dy * dy); + return Sqrt(dx * dx + dy * dy); } // Arcade has no linear referencing, so walk the vertices to find the point halfway along the line. function midpointAlongLine(geometry) { - var paths = geometry.paths; - if (IsEmpty(paths) || Count(paths) == 0) { - return null; - } - - var total = 0; - for (var p in paths) { - var path = paths[p]; - for (var v = 1; v < Count(path); v++) { - total += distance2d(path[v - 1], path[v]); - } - } - - var first = paths[0][0]; - if (total == 0) { - return { 'x': first.x, 'y': first.y }; - } - - var target = total / 2; - var traveled = 0; - for (var i in paths) { - var current = paths[i]; - for (var j = 1; j < Count(current); j++) { - var start = current[j - 1]; - var end = current[j]; - var length = distance2d(start, end); - - if (length > 0) { - if (traveled + length >= target) { - var ratio = (target - traveled) / length; - - return { - 'x': start.x + (end.x - start.x) * ratio, - 'y': start.y + (end.y - start.y) * ratio - }; - } - - traveled += length; - } - } - } - - return { 'x': first.x, 'y': first.y }; + var paths = geometry.paths; + if (IsEmpty(paths) || Count(paths) == 0) { + return null; + } + + var total = 0; + for (var p in paths) { + var path = paths[p]; + for (var v = 1; v < Count(path); v++) { + total += distance2d(path[v - 1], path[v]); + } + } + + var first = paths[0][0]; + if (total == 0) { + return { 'x': first.x, 'y': first.y }; + } + + var target = total / 2; + var traveled = 0; + for (var i in paths) { + var current = paths[i]; + for (var j = 1; j < Count(current); j++) { + var start = current[j - 1]; + var end = current[j]; + var length = distance2d(start, end); + + if (length > 0) { + if (traveled + length >= target) { + var ratio = (target - traveled) / length; + + return { + 'x': start.x + (end.x - start.x) * ratio, + 'y': start.y + (end.y - start.y) * ratio + }; + } + + traveled += length; + } + } + } + + return { 'x': first.x, 'y': first.y }; } // Rounds half up to match the legacy "+ .5 then cast to long" behavior. function trimCoordinate(value, leading) { - var digits = Text(Floor(value + 0.5)); + var digits = Text(Floor(value + 0.5)); - return Mid(digits, leading, Count(digits) - leading - 1); + return Mid(digits, leading, Count(digits) - leading - 1); } function isWholeNumber(value) { - if (value == '') { - return false; - } + if (value == '') { + return false; + } - for (var i = 0; i < Count(value); i++) { - if (Find(Mid(value, i, 1), DIGITS) == -1) { - return false; - } - } + for (var i = 0; i < Count(value); i++) { + if (Find(Mid(value, i, 1), DIGITS) == -1) { + return false; + } + } - return true; + return true; } // NAME/POSTDIR/POSTTYPE are computed directly because attribute rule execution order is not guaranteed, // so FULLNAME may still hold the pre-edit value. function nameSuffix() { - var name = Trim(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME))); - if (name == '') { - return ''; - } + var name = Trim(IIf(IsEmpty($feature.NAME), '', Text($feature.NAME))); + if (name == '') { + return ''; + } - var postdir = Trim(IIf(IsEmpty($feature.POSTDIR), '', Text($feature.POSTDIR))); - var posttype = Trim(IIf(IsEmpty($feature.POSTTYPE), '', Text($feature.POSTTYPE))); - var suffix = IIf(isWholeNumber(name), postdir, posttype); + var postdir = Trim(IIf(IsEmpty($feature.POSTDIR), '', Text($feature.POSTDIR))); + var posttype = Trim(IIf(IsEmpty($feature.POSTTYPE), '', Text($feature.POSTTYPE))); + var suffix = IIf(isWholeNumber(name), postdir, posttype); - var value = '_' + name; - if (suffix != '') { - value += '_' + suffix; - } + var value = '_' + name; + if (suffix != '') { + value += '_' + suffix; + } - return Replace(value, ' ', '_'); + return Replace(value, ' ', '_'); } var geometry = Geometry($feature); if (IsEmpty(geometry)) { - return "ERR:MISSING_GEOMETRY"; + return "ERR:MISSING_GEOMETRY"; } var midpoint = midpointAlongLine(geometry); if (IsEmpty(midpoint)) { - return "ERR:MISSING_MIDPOINT"; + return "ERR:MISSING_MIDPOINT"; } var midpointGeom = Point({ 'x': midpoint.x, 'y': midpoint.y, 'spatialReference': geometry.spatialReference }); var nationalGrid = FeatureSetByName($datastore, 'NationalGrid', ['GRID1MIL', 'GRID100K'], true); var gridMatch = First(Intersects(nationalGrid, midpointGeom)); if (IsEmpty(gridMatch)) { - return "ERR:NO_GRID_MATCH"; + return "ERR:NO_GRID_MATCH"; } var grid1mil = gridMatch.GRID1MIL; diff --git a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade index 340d302..c2fd252 100644 --- a/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade +++ b/attribute-rules/constraint/Roads_Edit/constrain_to_attribute_domain.arcade @@ -3,48 +3,48 @@ // ERROR_MESSAGE: A field value does not match its coded-value domain. // Pre-declare fields to the Pro Arcade parser Expects($feature, - 'PREDIR', 'POSTTYPE', 'POSTDIR', 'AN_POSTDIR', - 'A1_PREDIR', 'A1_POSTTYPE', 'A1_POSTDIR', - 'A2_PREDIR', 'A2_POSTTYPE', 'A2_POSTDIR', - 'VERT_LEVEL', 'ONEWAY', 'SPEED_LMT' + 'PREDIR', 'POSTTYPE', 'POSTDIR', 'AN_POSTDIR', + 'A1_PREDIR', 'A1_POSTTYPE', 'A1_POSTDIR', + 'A2_PREDIR', 'A2_POSTTYPE', 'A2_POSTDIR', + 'VERT_LEVEL', 'ONEWAY', 'SPEED_LMT' ); // Array of all fields to validate against their assigned domains var fieldsToCheck = [ - 'PREDIR', 'POSTTYPE', 'POSTDIR', 'AN_POSTDIR', - 'A1_PREDIR', 'A1_POSTTYPE', 'A1_POSTDIR', - 'A2_PREDIR', 'A2_POSTTYPE', 'A2_POSTDIR', - 'VERT_LEVEL', 'ONEWAY', 'SPEED_LMT' + 'PREDIR', 'POSTTYPE', 'POSTDIR', 'AN_POSTDIR', + 'A1_PREDIR', 'A1_POSTTYPE', 'A1_POSTDIR', + 'A2_PREDIR', 'A2_POSTTYPE', 'A2_POSTDIR', + 'VERT_LEVEL', 'ONEWAY', 'SPEED_LMT' ]; for (var i in fieldsToCheck) { - var fieldName = fieldsToCheck[i]; - var val = $feature[fieldName]; - - // Skip empty/null values (let mandatory field settings handle null checks) - if (IsEmpty(val)) { continue; } - - // Retrieve the domain assigned to this specific field - var dom = Domain($feature, fieldName); - - // If a domain is attached, validate the code - if (!IsEmpty(dom) && HasKey(dom, 'codedValues')) { - var isValid = false; - var validCodes = dom.codedValues; - - for (var j in validCodes) { - if (validCodes[j].code == val) { - isValid = true; - break; - } - } - - // If the value wasn't found in the domain, trigger the failure immediately - if (!isValid) { - return { - 'errorMessage': 'Invalid value [' + Text(val) + '] entered for field: ' + fieldName - }; - } + var fieldName = fieldsToCheck[i]; + var val = $feature[fieldName]; + + // Skip empty/null values (let mandatory field settings handle null checks) + if (IsEmpty(val)) { continue; } + + // Retrieve the domain assigned to this specific field + var dom = Domain($feature, fieldName); + + // If a domain is attached, validate the code + if (!IsEmpty(dom) && HasKey(dom, 'codedValues')) { + var isValid = false; + var validCodes = dom.codedValues; + + for (var j in validCodes) { + if (validCodes[j].code == val) { + isValid = true; + break; } + } + + // If the value wasn't found in the domain, trigger the failure immediately + if (!isValid) { + return { + 'errorMessage': 'Invalid value [' + Text(val) + '] entered for field: ' + fieldName + }; + } + } } return true; // All fields passed validation From 5dbc444bbe73400e30cdcce801b73936e1eba2c0 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 16:12:17 -0600 Subject: [PATCH 08/11] docs: README refactor --- README.md | 70 +------------------------------------- attribute-rules/README.md | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 69 deletions(-) create mode 100644 attribute-rules/README.md diff --git a/README.md b/README.md index 03eaed8..dc5e0b2 100644 --- a/README.md +++ b/README.md @@ -4,75 +4,7 @@ Tools for working with UTRANS data ## Attribute Rule Scripts -The attribute-rule utilities are in [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py) and [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py). - -- [attribute-rules/deploy_ar.py](attribute-rules/deploy_ar.py) is the primary script. - - Default mode is idempotent deploy: add if missing, alter if mutable changes, recreate when immutable changes require it. - - Use `--mode add` for strict add-only behavior. - - Use `--mode alter` to update an existing rule only. -- [attribute-rules/deploy_all_ar.py](attribute-rules/deploy_all_ar.py) recursively deploys every `.arcade` file using idempotent deploy mode. It infers the rule type from the top-level folder (`calculation`, `constraint`, or `validation`) and reads optional `// KEY: value` metadata from each script. - -Supported per-script metadata keys are `RULE_NAME`, `FIELD`, `EVENTS`, `IS_EDITABLE`, `ERROR_NUMBER`, `ERROR_MESSAGE`, `DESCRIPTION`, and `SUBTYPE`. Use `FIELD:` with no value for calculation rules that return an `attributes` dictionary for multiple fields. - -Examples: - -```bash -# deploy mode (default) -python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --field FULLNAME - -# add-only behavior -python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode add --field FULLNAME - -# alter existing rule only -python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode alter --field FULLNAME - -# recursively deploy every rule using its script metadata -python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" - -# preview all actions without changing the geodatabase -python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" --dry-run -``` - -### Deploying to an enterprise geodatabase - -Attribute rule tools require a file, mobile, or enterprise geodatabase dataset. A feature service URL such as `https://server/server/rest/services/Roads/FeatureServer/0` cannot be used as `--in-table`, and ArcGIS Online hosted feature layers are not a supported target. Deploy to the enterprise geodatabase instead. - -The UTRANS enterprise geodatabase is SQL Server on `db.ugrc.utah.gov`. Create a connection file once, then use its `.sde` path in place of a local `.gdb` path. - -Option 1 β€” ArcGIS Pro: use the **New Database Connection** dialog in the Catalog pane (Database Platform `SQL Server`, Instance `db.ugrc.utah.gov`, Authentication Type `Database authentication`, User name `TRANSADMIN`). Then right-click the finished connection and choose **Copy Path** to get the `.sde` location to pass to the script. Connections saved to Catalog favorites live in `%APPDATA%\Esri\ArcGISPro\Favorites`, so you can also browse to the file directly. - -Option 2 β€” Python: - -```python -import arcpy - -arcpy.management.CreateDatabaseConnection( - out_folder_path="Z:/connections", - out_name="roadtest_transadmin.sde", - database_platform="SQL_SERVER", - instance="db.ugrc.utah.gov", - account_authentication="DATABASE_AUTH", - username="TRANSADMIN", - save_user_pass="SAVE_USERNAME", - database="RoadTest", -) -``` - -Append the dataset name to the connection path and deploy: - -```bash -python attribute-rules/deploy_all_ar.py --in-table "Z:/connections/roadtest.sde/TRANSADMIN.Roads_Edit" --dry-run -``` - -Requirements to be aware of: - -- Connect as the dataset owner (`TRANSADMIN`). Attribute rule changes made by any other account fail validation. -- The identity that matters is the database login stored in the `.sde` file, not your Windows account or ArcGIS Pro portal sign-in. Check it with `arcpy.Describe(".sde").connectionProperties.user`. -- Validation rules require branch versioning on an enterprise geodatabase. Calculation and constraint rules do not. -- Datasets are qualified as `schema.table`. Feature classes inside a feature dataset can also be addressed as `.sde/TRANSADMIN.Centerlines_Edit/TRANSADMIN.Roads_Edit`. -- Always run `--dry-run` first. It previews each rule action and warns about owner and versioning problems, but because it never calls the geoprocessing tool it cannot catch every server-side rejection. -- Confirm a connection's platform with `arcpy.Describe(".sde").connectionProperties.dbclient`. -- Keep connection files outside the repository so database credentials are never committed. +These are Arcade scripts for calculation and constraint rules on the UTRANS geodatabase in ArcGIS Pro. See its [README](attribute-rules/README.md) for deployment instructions. ## `Get_Recent_Edits.py` script run example diff --git a/attribute-rules/README.md b/attribute-rules/README.md new file mode 100644 index 0000000..e01dfcb --- /dev/null +++ b/attribute-rules/README.md @@ -0,0 +1,71 @@ +# Attribute Rules + +The attribute-rule utilities are in [deploy_ar.py](deploy_ar.py) and [deploy_all_ar.py](deploy_all_ar.py). + +- [deploy_ar.py](deploy_ar.py) is the primary script. + - Default mode is idempotent deploy: add if missing, alter if mutable changes, recreate when immutable changes require it. + - Use `--mode add` for strict add-only behavior. + - Use `--mode alter` to update an existing rule only. +- [deploy_all_ar.py](deploy_all_ar.py) recursively deploys every `.arcade` file using idempotent deploy mode. It infers the rule type from the top-level folder (`calculation`, `constraint`, or `validation`) and reads optional `// KEY: value` metadata from each script. + +Supported per-script metadata keys are `RULE_NAME`, `FIELD`, `EVENTS`, `IS_EDITABLE`, `ERROR_NUMBER`, `ERROR_MESSAGE`, `DESCRIPTION`, and `SUBTYPE`. Use `FIELD:` with no value for calculation rules that return an `attributes` dictionary for multiple fields. + +Examples: + +```bash +# deploy mode (default) +python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --field FULLNAME + +# add-only behavior +python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode add --field FULLNAME + +# alter existing rule only +python attribute-rules/deploy_ar.py fullname_calculation --in-table "Z:/data/County.gdb/Roads" --mode alter --field FULLNAME + +# recursively deploy every rule using its script metadata +python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" + +# preview all actions without changing the geodatabase +python attribute-rules/deploy_all_ar.py --in-table "Z:/data/County.gdb/Roads" --dry-run +``` + +## Deploying to an enterprise geodatabase + +Attribute rule tools require a file, mobile, or enterprise geodatabase dataset. A feature service URL such as `https://server/server/rest/services/Roads/FeatureServer/0` cannot be used as `--in-table`, and ArcGIS Online hosted feature layers are not a supported target. Deploy to the enterprise geodatabase instead. + +The UTRANS enterprise geodatabase is SQL Server on `db.ugrc.utah.gov`. Create a connection file once, then use its `.sde` path in place of a local `.gdb` path. + +Option 1 β€” ArcGIS Pro: use the **New Database Connection** dialog in the Catalog pane (Database Platform `SQL Server`, Instance `db.ugrc.utah.gov`, Authentication Type `Database authentication`, User name `TRANSADMIN`). Then right-click the finished connection and choose **Copy Path** to get the `.sde` location to pass to the script. Connections saved to Catalog favorites live in `%APPDATA%\Esri\ArcGISPro\Favorites`, so you can also browse to the file directly. + +Option 2 β€” Python: + +```python +import arcpy + +arcpy.management.CreateDatabaseConnection( + out_folder_path="Z:/connections", + out_name="roadtest_transadmin.sde", + database_platform="SQL_SERVER", + instance="db.ugrc.utah.gov", + account_authentication="DATABASE_AUTH", + username="TRANSADMIN", + save_user_pass="SAVE_USERNAME", + database="RoadTest", +) +``` + +Append the dataset name to the connection path and deploy: + +```bash +python attribute-rules/deploy_all_ar.py --in-table "Z:/connections/roadtest.sde/TRANSADMIN.Roads_Edit" --dry-run +``` + +Requirements to be aware of: + +- Connect as the dataset owner (`TRANSADMIN`). Attribute rule changes made by any other account fail validation. +- The identity that matters is the database login stored in the `.sde` file, not your Windows account or ArcGIS Pro portal sign-in. Check it with `arcpy.Describe(".sde").connectionProperties.user`. +- Validation rules require branch versioning on an enterprise geodatabase. Calculation and constraint rules do not. +- Datasets are qualified as `schema.table`. Feature classes inside a feature dataset can also be addressed as `.sde/TRANSADMIN.Centerlines_Edit/TRANSADMIN.Roads_Edit`. +- Always run `--dry-run` first. It previews each rule action and warns about owner and versioning problems, but because it never calls the geoprocessing tool it cannot catch every server-side rejection. +- Confirm a connection's platform with `arcpy.Describe(".sde").connectionProperties.dbclient`. +- Keep connection files outside the repository so database credentials are never committed. From f23d240b530d0f2bcb9e569898eb386d9d2c4a14 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Mon, 31 Aug 2026 16:45:46 -0600 Subject: [PATCH 09/11] fix: repaired to proper midpoint computation --- .../Roads_Edit/calculate_dot_owner.arcade | 61 +----------------- .../calculation/Roads_Edit/unique_id.arcade | 63 ++----------------- 2 files changed, 7 insertions(+), 117 deletions(-) diff --git a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade index a554d24..6798110 100644 --- a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade +++ b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade @@ -1,62 +1,8 @@ // Calculates DOT_OWN_L and DOT_OWN_R -// FIELD: +// FIELD: DOT_OWN_L // EVENTS: INSERT, UPDATE Expects($feature, 'DOT_OWN_L', 'DOT_OWN_R', 'DOT_RTNAME'); -function distance2d(from, to) { - var dx = to.x - from.x; - var dy = to.y - from.y; - - return Sqrt(dx * dx + dy * dy); -} - -// Arcade has no linear referencing, so walk the vertices to find the point halfway along the line. -function midpointAlongLine(geo) { - var paths = geo.paths; - if (IsEmpty(paths) || Count(paths) == 0) { - return null; - } - - var total = 0; - for (var p in paths) { - var path = paths[p]; - for (var v = 1; v < Count(path); v++) { - total += distance2d(path[v - 1], path[v]); - } - } - - var firstInPaths = paths[0][0]; - if (total == 0) { - return { 'x': firstInPaths.x, 'y': firstInPaths.y }; - } - - var target = total / 2; - var traveled = 0; - for (var i in paths) { - var current = paths[i]; - for (var j = 1; j < Count(current); j++) { - var start = current[j - 1]; - var end = current[j]; - var len = distance2d(start, end); - - if (len > 0) { - if (traveled + len >= target) { - var ratio = (target - traveled) / len; - - return { - 'x': start.x + (end.x - start.x) * ratio, - 'y': start.y + (end.y - start.y) * ratio - }; - } - - traveled += len; - } - } - } - - return { 'x': firstInPaths.x, 'y': firstInPaths.y }; -} - var dotRtName = Trim(IIf(IsEmpty($feature.DOT_RTNAME), '', Text($feature.DOT_RTNAME))); var dotOwnL = ''; var dotOwnR = ''; @@ -66,12 +12,11 @@ if (Left(dotRtName, 1) == '0') { } else { // 1. Get the line geometry and calculate the midpoint (50% along line) var lineGeom = Geometry($feature); - var midPt = midpointAlongLine(lineGeom); // use the custom midpoint function // 2. Create left (-15m) and right (+15m) offset points // Note: Offset syntax depends on unit settings, but conceptually: - var leftPt = Point({ 'x': midPt.x - 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); - var rightPt = Point({ 'x': midPt.x + 15, 'y': midPt.y, 'spatialReference': lineGeom.spatialReference }); + var leftPt = Centroid(Offset($feature, -15, 'meters'), "labelPoint"); + var rightPt = Centroid(Offset($feature, 15, 'meters'), "labelPoint"); // 3. Query the Municipalities polygon FeatureSet var muniFS = FeatureSetByName($datastore, "Municipalities", ["NAME"], true); diff --git a/attribute-rules/calculation/Roads_Edit/unique_id.arcade b/attribute-rules/calculation/Roads_Edit/unique_id.arcade index d3dc942..6bb4ff8 100644 --- a/attribute-rules/calculation/Roads_Edit/unique_id.arcade +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -1,4 +1,4 @@ -/* global $feature, $datastore, $layer, $map, FeatureSetByName, Intersects, First, Point, IIf, Buffer, Expects, Geometry, IsEmpty, Trim, Text, Count, Sqrt, Floor, Find, Mid, Replace */ +/* global $feature, $datastore, $layer, $map, FeatureSetByName, Intersects, First, Centroid, IIf, Buffer, Expects, Geometry, IsEmpty, Trim, Text, Count, Floor, Find, Mid, Replace */ // Ports the legacy UtransEditor getNewUniqueID logic, reading GRID1MIL/GRID100K from the USNG. // Grid-cell zone mismatches at the UTM 11/12 boundary are ignored since this ID is only eyeballed, not parsed. // FIELD: UNIQUE_ID @@ -7,60 +7,6 @@ Expects($feature, 'NAME', 'POSTDIR', 'POSTTYPE', 'UNIQUE_ID'); var DIGITS = '0123456789'; -function distance2d(from, to) { - var dx = to.x - from.x; - var dy = to.y - from.y; - - return Sqrt(dx * dx + dy * dy); -} - -// Arcade has no linear referencing, so walk the vertices to find the point halfway along the line. -function midpointAlongLine(geometry) { - var paths = geometry.paths; - if (IsEmpty(paths) || Count(paths) == 0) { - return null; - } - - var total = 0; - for (var p in paths) { - var path = paths[p]; - for (var v = 1; v < Count(path); v++) { - total += distance2d(path[v - 1], path[v]); - } - } - - var first = paths[0][0]; - if (total == 0) { - return { 'x': first.x, 'y': first.y }; - } - - var target = total / 2; - var traveled = 0; - for (var i in paths) { - var current = paths[i]; - for (var j = 1; j < Count(current); j++) { - var start = current[j - 1]; - var end = current[j]; - var length = distance2d(start, end); - - if (length > 0) { - if (traveled + length >= target) { - var ratio = (target - traveled) / length; - - return { - 'x': start.x + (end.x - start.x) * ratio, - 'y': start.y + (end.y - start.y) * ratio - }; - } - - traveled += length; - } - } - } - - return { 'x': first.x, 'y': first.y }; -} - // Rounds half up to match the legacy "+ .5 then cast to long" behavior. function trimCoordinate(value, leading) { var digits = Text(Floor(value + 0.5)); @@ -107,12 +53,11 @@ if (IsEmpty(geometry)) { return "ERR:MISSING_GEOMETRY"; } -var midpoint = midpointAlongLine(geometry); -if (IsEmpty(midpoint)) { +var midpointGeom = Centroid(geometry, "labelPoint"); +if (IsEmpty(midpointGeom)) { return "ERR:MISSING_MIDPOINT"; } -var midpointGeom = Point({ 'x': midpoint.x, 'y': midpoint.y, 'spatialReference': geometry.spatialReference }); var nationalGrid = FeatureSetByName($datastore, 'NationalGrid', ['GRID1MIL', 'GRID100K'], true); var gridMatch = First(Intersects(nationalGrid, midpointGeom)); if (IsEmpty(gridMatch)) { @@ -123,4 +68,4 @@ var grid1mil = gridMatch.GRID1MIL; var grid100k = gridMatch.GRID100K; // Test case: midpoint 12T VK 3756 1234 on "GOLD KEY LN" -> "12TVK37561234_GOLD_KEY_LN" -return Trim(grid1mil + grid100k + trimCoordinate(midpoint.x, 1) + trimCoordinate(midpoint.y, 2) + nameSuffix()); +return Trim(grid1mil + grid100k + trimCoordinate(midpointGeom.x, 1) + trimCoordinate(midpointGeom.y, 2) + nameSuffix()); From 97d4c8e2a95f7ed63c8b9534a5b6d4b900fe459c Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Tue, 1 Sep 2026 10:15:50 -0600 Subject: [PATCH 10/11] refactor: AR combination --- .../Roads_Edit/calculate_dot_owner.arcade | 62 ------------------- .../Roads_Edit/spatial_assignments.arcade | 58 ++++++++++++----- .../test/spatial_calculations.test.ts | 9 +-- 3 files changed, 45 insertions(+), 84 deletions(-) delete mode 100644 attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade diff --git a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade b/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade deleted file mode 100644 index 6798110..0000000 --- a/attribute-rules/calculation/Roads_Edit/calculate_dot_owner.arcade +++ /dev/null @@ -1,62 +0,0 @@ -// Calculates DOT_OWN_L and DOT_OWN_R -// FIELD: DOT_OWN_L -// EVENTS: INSERT, UPDATE -Expects($feature, 'DOT_OWN_L', 'DOT_OWN_R', 'DOT_RTNAME'); - -var dotRtName = Trim(IIf(IsEmpty($feature.DOT_RTNAME), '', Text($feature.DOT_RTNAME))); -var dotOwnL = ''; -var dotOwnR = ''; -if (Left(dotRtName, 1) == '0') { - dotOwnL = 'UDOT'; - dotOwnR = 'UDOT'; -} else { - // 1. Get the line geometry and calculate the midpoint (50% along line) - var lineGeom = Geometry($feature); - - // 2. Create left (-15m) and right (+15m) offset points - // Note: Offset syntax depends on unit settings, but conceptually: - var leftPt = Centroid(Offset($feature, -15, 'meters'), "labelPoint"); - var rightPt = Centroid(Offset($feature, 15, 'meters'), "labelPoint"); - - // 3. Query the Municipalities polygon FeatureSet - var muniFS = FeatureSetByName($datastore, "Municipalities", ["NAME"], true); - var leftMuni = First(Intersects(muniFS, leftPt)); - var rightMuni = First(Intersects(muniFS, rightPt)); - - // 4. Query the Counties polygon FeatureSet - var countyFS = FeatureSetByName($datastore, "Counties", ["NAME"], true); - - // 5. Determine DOT_OWN_L - if (!IsEmpty(leftMuni)) { - dotOwnL = 'MU ' + leftMuni.NAME; - } else { - var leftCounty = First(Intersects(countyFS, leftPt)); - if (!IsEmpty(leftCounty)) { - // Convert NAME as such: KANE -> Kane - dotOwnL = 'CO ' + Proper(leftCounty.NAME); - } else { - dotOwnL = ''; - } - } - - // 6. Determine DOT_OWN_R - if (!IsEmpty(rightMuni)) { - dotOwnR = 'MU ' + rightMuni.NAME; - } else { - var rightCounty = First(Intersects(countyFS, rightPt)); - if (!IsEmpty(rightCounty)) { - dotOwnR = 'CO ' + Proper(rightCounty.NAME); - } else { - dotOwnR = ''; - } - } -} - -return { - "result": { - "attributes": { - "DOT_OWN_L": dotOwnL, - "DOT_OWN_R": dotOwnR - } - } -}; \ No newline at end of file diff --git a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade index 98c57db..64a7314 100644 --- a/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade +++ b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade @@ -1,9 +1,10 @@ /* global $feature, $datastore, $layer, $map, FeatureSetByName, IIf, Buffer */ // FIELD: // EVENTS: INSERT, UPDATE +Expects($feature, 'DOT_RTNAME'); var addressSystemQuadrantsLayer = FeatureSetByName($datastore, "AddressSystemQuadrants", ["QUADRANT", "GRID_NAME"]); -var countyLayer = FeatureSetByName($datastore, "Counties", ["FIPS_STR"]); +var countyLayer = FeatureSetByName($datastore, "Counties", ["FIPS_STR", "NAME"]); var zipCodeLayer = FeatureSetByName($datastore, "ZipCodes", ["ZIP5", "NAME"]); var municipalityLayer = FeatureSetByName($datastore, "Municipalities", ["NAME"]); @@ -12,12 +13,24 @@ var roadRight = Centroid(Offset($feature, 15, 'meters', 'square'), 'labelPoint') function fieldOrNull(feature, fieldName) { if (IsEmpty(feature)) { - return null; + return null; } return feature[fieldName]; } +function dotOwner(municipalityFeature, countyFeature) { + if (!IsEmpty(municipalityFeature)) { + return 'MU ' + municipalityFeature.NAME; + } + + if (!IsEmpty(countyFeature)) { + return 'CO ' + Proper(countyFeature.NAME); + } + + return ''; +} + var quadrantLeftFeature = First(Intersects(addressSystemQuadrantsLayer, roadLeft)); var quadrantRightFeature = First(Intersects(addressSystemQuadrantsLayer, roadRight)); var countyLeftFeature = First(Intersects(countyLayer, roadLeft)); @@ -27,21 +40,34 @@ var zipCodeRightFeature = First(Intersects(zipCodeLayer, roadRight)); var municipalityLeftFeature = First(Intersects(municipalityLayer, roadLeft)); var municipalityRightFeature = First(Intersects(municipalityLayer, roadRight)); +var dotRtName = Trim(IIf(IsEmpty($feature.DOT_RTNAME), '', Text($feature.DOT_RTNAME))); +var dotOwnL = ''; +var dotOwnR = ''; +if (Left(dotRtName, 1) == '0') { + dotOwnL = 'UDOT'; + dotOwnR = 'UDOT'; +} else { + dotOwnL = dotOwner(municipalityLeftFeature, countyLeftFeature); + dotOwnR = dotOwner(municipalityRightFeature, countyRightFeature); +} + return { "result": { - "attributes": { - "QUADRANT_L": fieldOrNull(quadrantLeftFeature, "QUADRANT"), - "QUADRANT_R": fieldOrNull(quadrantRightFeature, "QUADRANT"), - "COUNTY_L": fieldOrNull(countyLeftFeature, "FIPS_STR"), - "COUNTY_R": fieldOrNull(countyRightFeature, "FIPS_STR"), - "ADDRSYS_L": fieldOrNull(quadrantLeftFeature, "GRID_NAME"), - "ADDRSYS_R": fieldOrNull(quadrantRightFeature, "GRID_NAME"), - "POSTCOMM_L": fieldOrNull(zipCodeLeftFeature, "NAME"), - "POSTCOMM_R": fieldOrNull(zipCodeRightFeature, "NAME"), - "ZIPCODE_L": fieldOrNull(zipCodeLeftFeature, "ZIP5"), - "ZIPCODE_R": fieldOrNull(zipCodeRightFeature, "ZIP5"), - "INCMUNI_L": fieldOrNull(municipalityLeftFeature, "NAME"), - "INCMUNI_R": fieldOrNull(municipalityRightFeature, "NAME") - } + "attributes": { + "QUADRANT_L": fieldOrNull(quadrantLeftFeature, "QUADRANT"), + "QUADRANT_R": fieldOrNull(quadrantRightFeature, "QUADRANT"), + "COUNTY_L": fieldOrNull(countyLeftFeature, "FIPS_STR"), + "COUNTY_R": fieldOrNull(countyRightFeature, "FIPS_STR"), + "ADDRSYS_L": fieldOrNull(quadrantLeftFeature, "GRID_NAME"), + "ADDRSYS_R": fieldOrNull(quadrantRightFeature, "GRID_NAME"), + "POSTCOMM_L": fieldOrNull(zipCodeLeftFeature, "NAME"), + "POSTCOMM_R": fieldOrNull(zipCodeRightFeature, "NAME"), + "ZIPCODE_L": fieldOrNull(zipCodeLeftFeature, "ZIP5"), + "ZIPCODE_R": fieldOrNull(zipCodeRightFeature, "ZIP5"), + "INCMUNI_L": fieldOrNull(municipalityLeftFeature, "NAME"), + "INCMUNI_R": fieldOrNull(municipalityRightFeature, "NAME"), + "DOT_OWN_L": dotOwnL, + "DOT_OWN_R": dotOwnR + } } } diff --git a/attribute-rules/test/spatial_calculations.test.ts b/attribute-rules/test/spatial_calculations.test.ts index 4741144..f8a9b8c 100644 --- a/attribute-rules/test/spatial_calculations.test.ts +++ b/attribute-rules/test/spatial_calculations.test.ts @@ -22,9 +22,6 @@ function emptyDatastore() { const spatialExecutor = await loadExecutor("calculation/Roads_Edit/spatial_assignments.arcade", [ { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } ]); -const ownerExecutor = await loadExecutor("calculation/Roads_Edit/calculate_dot_owner.arcade", [ - { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } -]); const uniqueIdExecutor = await loadExecutor("calculation/Roads_Edit/unique_id.arcade", [ { name: "$feature", type: "feature" }, { name: "$datastore", type: "featureSetCollection" } ]); @@ -33,11 +30,11 @@ test("spatial_assignments compiles with its datastore contract", () => { expect(spatialExecutor).toBeDefined(); }); -test("calculate_dot_owner assigns UDOT to both sides for zero-prefixed routes", async () => { - const result = await ownerExecutor.executeAsync({ +test("spatial_assignments assigns UDOT to both sides for zero-prefixed routes", async () => { + const result = await spatialExecutor.executeAsync({ "$feature": feature({ DOT_RTNAME: "0123" }, lineGeometry), "$datastore": emptyDatastore() }); - expect(result).toEqual({ result: { attributes: { DOT_OWN_L: "UDOT", DOT_OWN_R: "UDOT" } } }); + expect(result.result.attributes).toMatchObject({ DOT_OWN_L: "UDOT", DOT_OWN_R: "UDOT" }); }); test("unique_id reports missing geometry", async () => { From ed39a3773588e9119dbd14ef644941945f668762 Mon Sep 17 00:00:00 2001 From: Alex Neville Date: Tue, 1 Sep 2026 11:20:09 -0600 Subject: [PATCH 11/11] fix: unit test failure fixed --- .vscode/settings.json | 3 ++- attribute-rules/test/_helpers.ts | 5 +++-- attribute-rules/test/setup.ts | 8 ++++++++ attribute-rules/test/spatial_calculations.test.ts | 2 +- attribute-rules/vitest.config.ts | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 attribute-rules/test/setup.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f52c7c8..d5124d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -89,5 +89,6 @@ "editor.tabSize": 2, "editor.detectIndentation": false }, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "python-envs.defaultPackageManager": "ms-python.python:conda" } diff --git a/attribute-rules/test/_helpers.ts b/attribute-rules/test/_helpers.ts index 3b8b0b3..df29174 100644 --- a/attribute-rules/test/_helpers.ts +++ b/attribute-rules/test/_helpers.ts @@ -15,8 +15,9 @@ export function feature(attributes: Record, geometry?: any) { return new Graphic({ attributes, geometry }); } +// NAD83 UTM 12N to match Roads_Edit; Offset() uses linear units and rejects a geographic SR export const lineGeometry = { type: "polyline", - paths: [[[-111.9, 40.7], [-111.8, 40.7]]], - spatialReference: { wkid: 4326 } + paths: [[[424100, 4505000], [432500, 4505000]]], + spatialReference: { wkid: 26912 } }; \ No newline at end of file diff --git a/attribute-rules/test/setup.ts b/attribute-rules/test/setup.ts new file mode 100644 index 0000000..c9a3550 --- /dev/null +++ b/attribute-rules/test/setup.ts @@ -0,0 +1,8 @@ +// @arcgis/core falls back to an in-process "worker" built on a DocumentFragment when +// no Worker exists (WorkerFallback.js). Node has neither, so expose just the three +// EventTarget methods that fallback consumes instead of pulling in a full DOM. +if (typeof globalThis.document === "undefined") { + (globalThis as any).document = { + createDocumentFragment: () => new EventTarget() + }; +} diff --git a/attribute-rules/test/spatial_calculations.test.ts b/attribute-rules/test/spatial_calculations.test.ts index f8a9b8c..1aee264 100644 --- a/attribute-rules/test/spatial_calculations.test.ts +++ b/attribute-rules/test/spatial_calculations.test.ts @@ -5,7 +5,7 @@ import { feature, lineGeometry, loadExecutor } from "./_helpers.js"; function emptyPolygonLayer(title: string, fields: any[]) { return new FeatureLayer({ - title, geometryType: "polygon", spatialReference: { wkid: 4326 }, + title, geometryType: "polygon", spatialReference: { wkid: 26912 }, objectIdField: "OBJECTID", fields: [{ name: "OBJECTID", type: "oid" }, ...fields], source: [] }); } diff --git a/attribute-rules/vitest.config.ts b/attribute-rules/vitest.config.ts index 09720e7..6b8b17e 100644 --- a/attribute-rules/vitest.config.ts +++ b/attribute-rules/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + setupFiles: ["test/setup.ts"], include: ["test/**/*.test.ts"] } }); \ No newline at end of file