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..d5124d3 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 @@ -74,5 +79,16 @@ "[xaml]": { "editor.formatOnSave": false }, - "editor.formatOnSave": true + "[python]": { + "editor.insertSpaces": true, + "editor.tabSize": 4, + "editor.detectIndentation": false + }, + "[javascript]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.detectIndentation": false + }, + "editor.formatOnSave": true, + "python-envs.defaultPackageManager": "ms-python.python:conda" } diff --git a/README.md b/README.md index 80fd133..dc5e0b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ Tools for working with UTRANS data -## CLI +## Attribute Rule Scripts + +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 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/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. 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..f254ca8 --- /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..0bf3bf0 --- /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..dbc1d5f --- /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..64a7314 --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/spatial_assignments.arcade @@ -0,0 +1,73 @@ +/* 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", "NAME"]); +var zipCodeLayer = FeatureSetByName($datastore, "ZipCodes", ["ZIP5", "NAME"]); +var municipalityLayer = FeatureSetByName($datastore, "Municipalities", ["NAME"]); + +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; + } + + 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)); +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)); + +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"), + "DOT_OWN_L": dotOwnL, + "DOT_OWN_R": dotOwnR + } + } +} 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..6bb4ff8 --- /dev/null +++ b/attribute-rules/calculation/Roads_Edit/unique_id.arcade @@ -0,0 +1,71 @@ +/* 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 +// EVENTS: INSERT, UPDATE +Expects($feature, 'NAME', 'POSTDIR', 'POSTTYPE', 'UNIQUE_ID'); + +var DIGITS = '0123456789'; + +// 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 midpointGeom = Centroid(geometry, "labelPoint"); +if (IsEmpty(midpointGeom)) { + return "ERR:MISSING_MIDPOINT"; +} + +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(midpointGeom.x, 1) + trimCoordinate(midpointGeom.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..c2fd252 --- /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..9f93ecb --- /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/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..df29174 --- /dev/null +++ b/attribute-rules/test/_helpers.ts @@ -0,0 +1,23 @@ +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 }); +} + +// NAD83 UTM 12N to match Roads_Edit; Offset() uses linear units and rejects a geographic SR +export const lineGeometry = { + type: "polyline", + paths: [[[424100, 4505000], [432500, 4505000]]], + spatialReference: { wkid: 26912 } +}; \ 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/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 new file mode 100644 index 0000000..1aee264 --- /dev/null +++ b/attribute-rules/test/spatial_calculations.test.ts @@ -0,0 +1,45 @@ +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: 26912 }, + 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 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("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.result.attributes).toMatchObject({ 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..6b8b17e --- /dev/null +++ b/attribute-rules/vitest.config.ts @@ -0,0 +1,9 @@ +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