88import uuid
99from pathlib import Path
1010import re
11+ import hashlib
12+ import json
1113from typing import Dict , Iterable , List , Sequence
1214
1315import dash
2628 search_ckan_datasets ,
2729 search_ckan_datasets_wel_rch ,
2830)
29- from flopy_interactive .config import GRID_STANDARD_VAR
31+ from flopy_interactive .config import CKAN_BASE_URL , GRID_STANDARD_VAR
3032from flopy_interactive .data .download import download_ckan_resource
3133from flopy_interactive .data .grid import load_grid_resource
3234from flopy_interactive .data .rch import apply_rch_rate_update , build_rch_cells_for_periods , load_rch
3335from flopy_interactive .data .wel import (
3436 apply_rate_update ,
3537 build_cell_id_lookup ,
3638 collect_wel_cells_for_period_data ,
39+ get_wel_period_keys ,
3740 load_wel ,
3841)
3942from flopy_interactive .viz .color_modes import apply_color_mode
4245DATA_DIR = Path (os .environ .get ("FLOPY_DATA_DIR" , "ckan_data" ))
4346OUTPUT_WEL = Path (os .environ .get ("FLOPY_OUTPUT_WEL" , "barton_springs_updated.wel" ))
4447SUGGEST_TITLE_FILTER = "Barton Springs Edwards Aquifer"
48+ CKAN_URL = os .environ .get ("FLOPY_CKAN_URL" , CKAN_BASE_URL )
4549
4650
4751def get_datasets () -> List [Dict ]:
@@ -362,6 +366,22 @@ def _dataset_options_without_grid(datasets: List[Dict]) -> List[Dict[str, str]]:
362366 return filtered
363367
364368
369+ def _summarize_periods (periods : List [int ], total : int | None ) -> str :
370+ """Return a compact stress-period summary string."""
371+ if not periods :
372+ return f"All periods ({ total } )" if total else "All periods"
373+ unique = sorted ({int (p ) for p in periods })
374+ if total and len (unique ) >= total :
375+ return f"All periods ({ total } )"
376+ if total and total > 0 and len (unique ) / total >= 0.7 :
377+ return f"Periods: { len (unique )} /{ total } "
378+ if len (unique ) > 1 and unique [- 1 ] - unique [0 ] + 1 == len (unique ):
379+ return f"Periods: { unique [0 ] + 1 } -{ unique [- 1 ] + 1 } "
380+ if len (unique ) <= 5 :
381+ return "Periods: " + ", " .join (str (p + 1 ) for p in unique )
382+ return "Periods: " + ", " .join (str (p + 1 ) for p in unique [:3 ]) + f" (+{ len (unique ) - 3 } more)"
383+
384+
365385def _downsample_for_choropleth (gdf_valid , gdf_full , zoom : float | None ) -> pd .DataFrame :
366386 """Downsample grid polygons for choropleth rendering based on zoom."""
367387 if zoom is None :
@@ -685,8 +705,7 @@ def update_dataset_controls(loaded_dataset: str | None):
685705 return [], []
686706 data = load_dataset (loaded_dataset )
687707 wel = data ["wel" ]
688- spd = wel .stress_period_data .data
689- period_keys = sorted (list (spd .keys ())) if spd else [0 ]
708+ period_keys = get_wel_period_keys (wel ) or [0 ]
690709 period_options = [{"label" : f"SP { idx + 1 } " , "value" : idx } for idx in period_keys ]
691710 nlay = data ["nlay" ]
692711 layer_options = [{"label" : str (layer ), "value" : layer } for layer in range (1 , nlay + 1 )]
@@ -709,8 +728,7 @@ def update_color_period(loaded_dataset, color_by, current_value):
709728 return {"display" : "none" }, [], None
710729 data = load_dataset (loaded_dataset )
711730 wel = data ["wel" ]
712- spd = getattr (wel , "stress_period_data" , None )
713- spd_keys = sorted (list (spd .data .keys ())) if spd is not None and hasattr (spd , "data" ) else [0 ]
731+ spd_keys = get_wel_period_keys (wel ) or [0 ]
714732 options = [{"label" : f"SP { idx + 1 } " , "value" : idx } for idx in spd_keys ]
715733 if current_value in spd_keys :
716734 value = current_value
@@ -788,8 +806,7 @@ def update_periods_layers(
788806 if not period_options or not layer_options :
789807 data = load_dataset (loaded_dataset )
790808 wel = data ["wel" ]
791- spd = wel .stress_period_data .data
792- period_keys = sorted (list (spd .keys ())) if spd else [0 ]
809+ period_keys = get_wel_period_keys (wel ) or [0 ]
793810 period_options = [
794811 {"label" : f"SP { idx + 1 } " , "value" : idx } for idx in period_keys
795812 ]
@@ -925,6 +942,7 @@ def update_dataset_suggestions(username, jwt_token):
925942 Input ("load-counter" , "data" ),
926943 State ("name-seed" , "data" ),
927944 State ("last-loaded-dataset" , "data" ),
945+ State ("periods" , "options" ),
928946 State ("dataset-name" , "value" ),
929947 State ("output-wel" , "value" ),
930948 State ("source-url" , "value" ),
@@ -946,6 +964,7 @@ def suggest_names(
946964 _load_counter ,
947965 name_seed ,
948966 last_loaded_dataset ,
967+ period_options ,
949968 current_dataset_name ,
950969 current_output_name ,
951970 current_source_url ,
@@ -992,33 +1011,34 @@ def suggest_names(
9921011 suffix = "0% change"
9931012 else :
9941013 suffix = f"set-{ rate_value :.0f} "
995- base_name = _slugify (f"{ loaded_dataset } -{ name_seed } " )
1014+ period_total = len (period_options or [])
1015+ period_summary = _summarize_periods (list (periods or []), period_total or None )
1016+ change_spec = {
1017+ "flux_source" : flux_source ,
1018+ "rate_mode" : rate_mode ,
1019+ "new_rate" : rate_value ,
1020+ "periods" : sorted (list (periods or [])),
1021+ "layers" : sorted (list (layers or [])),
1022+ "add_missing" : bool (add_missing ),
1023+ "selection_count" : len (selected_ids or []),
1024+ "color_by" : color_by ,
1025+ "category" : category_value ,
1026+ }
1027+ change_hash = hashlib .sha1 (json .dumps (change_spec , sort_keys = True ).encode ("utf-8" )).hexdigest ()[:8 ]
1028+ base_name = _slugify (f"{ loaded_dataset } -{ change_hash } " )
9961029 dataset_name = current_dataset_name or base_name
9971030 output_ext = ".rch" if flux_source == "rch" else ".wel"
998- output_name = f"{ loaded_dataset } _{ suffix } { output_ext } "
1031+ output_name = f"{ loaded_dataset } _{ suffix } _ { change_hash } { output_ext } "
9991032 if not output_name .lower ().endswith (output_ext ):
10001033 output_name = f"{ Path (output_name ).stem } { output_ext } "
10011034 source_url = current_source_url
1002- if not source_url and jwt_token and loaded_dataset :
1003- try :
1004- details = ckanp .package_show (jwt_token , loaded_dataset )
1005- source_url = details .get ("url" )
1006- if not source_url :
1007- resources = details .get ("resources" , [])
1008- target_var = ckanp .RCH_STANDARD_VAR if flux_source == "rch" else ckanp .WEL_STANDARD_VAR
1009- target_res = next (
1010- (res for res in resources if resource_has_standard_var (res , target_var )),
1011- None ,
1012- )
1013- if target_res :
1014- source_url = target_res .get ("url" )
1015- except Exception :
1016- source_url = current_source_url
1035+ if not source_url and loaded_dataset :
1036+ source_url = f"{ CKAN_URL } /dataset/{ loaded_dataset } "
10171037 selection_count = len (selected_ids or [])
10181038 selection_desc = f"Selected cells: { selection_count } "
10191039 if color_by in ("GCD_Name" , "PGMA_Name" ) and category_value :
10201040 selection_desc = f"Category { color_by } = { category_value } "
1021- period_desc = "All periods" if not periods else f"Periods: { ', ' . join ( str ( p ) for p in periods ) } "
1041+ period_desc = period_summary
10221042 if flux_source == "rch" :
10231043 layer_desc = "Layers: n/a"
10241044 add_desc = "Add missing wells: n/a"
@@ -1234,6 +1254,16 @@ def apply_rate(
12341254 data = load_dataset (loaded_dataset )
12351255 wel = data ["wel" ]
12361256 gdf = data ["gdf" ]
1257+ print (
1258+ "[apply] "
1259+ f"dataset={ loaded_dataset } flux_source={ flux_source } "
1260+ f"rate_mode={ rate_mode } new_rate={ new_rate } "
1261+ f"periods={ periods } layers={ layers } add_missing={ add_missing } "
1262+ f"selected_count={ len (selected_ids )} "
1263+ f"dataset_name={ dataset_name } output={ output_wel } "
1264+ f"source_url={ source_url } change_summary={ change_summary } "
1265+ f"jwt={ 'yes' if jwt_token else 'no' } "
1266+ )
12371267 output_path = Path (output_wel or OUTPUT_WEL )
12381268 target_ext = ".rch" if flux_source == "rch" else ".wel"
12391269 if output_path .suffix .lower () != target_ext :
0 commit comments