Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ prepare_met = "tribs_adapter.workflows.prepare_met_forcings:PrepareMetWorkflow"
prepare_soils = "tribs_adapter.workflows.prepare_soil_parameters:PrepareSoilsWorkflow"
prepare_land_cover = "tribs_adapter.workflows.prepare_land_cover:PrepareLandCoverWorkflow"
run_simulation = "tribs_adapter.workflows.run_simulation:RunSimulationWorkflow"
delineate_hydrologic_features_from_point = "tribs_adapter.workflows.delineate_hydrologic_features_from_point:DelineateHydrologicFeaturesFromPointWorkflow"
generate_tin = "tribs_adapter.workflows.generate_tin:GenerateTinWorkflow"

[tool.pdm]
Expand Down
10 changes: 8 additions & 2 deletions tribs_adapter/services/tribs_spatial_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,16 @@ def get_extent_for_dataset(self, dataset, buffer_factor=1.00001):
bbox = json[response_location]['latLonBoundingBox']

if bbox is not None:
# Buffer the extent outward proportionally to its size. Note: scaling the
# coordinates themselves (e.g. minx / buffer_factor) shrinks the extent
# instead of buffering it when coordinates are negative (west/south).
buffer_x = (bbox['maxx'] - bbox['minx']) * (buffer_factor - 1)
buffer_y = (bbox['maxy'] - bbox['miny']) * (buffer_factor - 1)

# minx, miny, maxx, maxy
extent = [
bbox['minx'] / buffer_factor, bbox['miny'] / buffer_factor, bbox['maxx'] * buffer_factor,
bbox['maxy'] * buffer_factor
bbox['minx'] - buffer_x, bbox['miny'] - buffer_y, bbox['maxx'] + buffer_x,
bbox['maxy'] + buffer_y
]

# order extent to be min_x min_y max_x max_y
Expand Down
109 changes: 109 additions & 0 deletions tribs_adapter/workflow_steps/select_point_rws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
********************************************************************************
* Name: select_point_rws.py
* Author: Yue Sun
* Created On: Aug 20, 2026
* Copyright: (c) Aquaveo 2026
********************************************************************************
"""

import param
from tethysext.atcore.models.resource_workflow_steps import SpatialInputRWS


class PointAttributes(param.Parameterized):
point_name = param.String(
label="Name",
doc="Name of point that will be used to reference it in results.",
allow_None=False, #: Required
)


class SelectPointRWS(SpatialInputRWS):
"""
Workflow step used for selecting a point on the map in Delineate Hydrologic Features From Point workflow.

Options:
shapes(list): The types of shapes to allow. Any combination of 'points', 'lines', 'polygons', and/or 'extents'.
singular_name(str): Name to use when referring to a single feature in other areas of the user interface (e.g. "Detention Basin").
plural_name(str): Name to use when referring to multiple features in other areas of the user interface (e.g. "Detention Basins").
allow_shapefile(bool): Allow shapfile upload as spatial input. Defaults to True.
allow_drawing(bool): Allow manually drawing shapes. Defaults to True.
snapping_enabled(bool): Enabled snapping when drawing features. Defaults to True.
snapping_layer(dict): Specify a layer to snap to. Create a 1-dict where the key is the dot-path to the layer attribute to use in comparison and the value is the value to match (e.g. {'data.layer_id': 10}).
snapping_options(dict): Supported options include edge, vertex, pixelTolerance. See: https://openlayers.org/en/latest/apidoc/module-ol_interaction_Snap.html
allow_image(bool): Allow reference image upload as spatial input. Defaults to False.
""" # noqa: #501
CONTROLLER = 'tethysapp.tribs.controllers.workflow_steps.select_point_mwv.SelectPointMWV'
TYPE = 'select_point_workflow_step'

__mapper_args__ = {'polymorphic_identity': TYPE}

@property
def default_options(self):
default_options = super().default_options
default_options.update({
'shapes': ['points'],
'singular_name': 'Point',
'plural_name': 'Points',
'allow_shapefile': True,
'allow_drawing': True,
'attributes': PointAttributes(),
'max_features': 1,
})
return default_options

def validate(self):
"""
Validates parameter values of this step, including that all features fall within
the extent of the input raster (stashed on the step by SelectPointMWV).

Returns:
bool: True if data is valid, else Raise exception.

Raises:
ValueError
"""
super().validate()

raster_extent = self.get_attribute('raster_extent', None)
if not raster_extent:
return True

min_x, min_y, max_x, max_y = raster_extent

# Allow a small tolerance (1% of each dimension) so edge clicks are not rejected
tolerance_x = (max_x - min_x) * 0.01
tolerance_y = (max_y - min_y) * 0.01
min_x, min_y = min_x - tolerance_x, min_y - tolerance_y
max_x, max_y = max_x + tolerance_x, max_y + tolerance_y

geometry = self.get_parameter('geometry') or {}

for feature in geometry.get('features', []):
coordinates = feature.get('geometry', {}).get('coordinates', [])
for x, y in self._iter_coordinates(coordinates):
if not (min_x <= x <= max_x and min_y <= y <= max_y):
singular_name = self.options.get('singular_name', 'Feature').lower()
raise ValueError(
f'The {singular_name} must be located within the extent of the input raster.'
)

return True

@classmethod
def _iter_coordinates(cls, coordinates):
"""
Yield (x, y) pairs from arbitrarily nested GeoJSON coordinates (Point, LineString, Polygon, Multi*).

Args:
coordinates(list): The coordinates member of a GeoJSON geometry.

Yields:
tuple: (x, y) coordinate pairs.
"""
if coordinates and isinstance(coordinates[0], (int, float)):
yield coordinates[0], coordinates[1]
else:
for nested in coordinates:
yield from cls._iter_coordinates(nested)
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
********************************************************************************
* Name: delineate_hydrologic_features_from_point_workflow
* Author: Yue Sun
* Created On: August 15, 2026
* Copyright: (c) Aquaveo 2026
********************************************************************************
"""
import os

from tribs_adapter.app_users import TribsRoles
from tethysext.atcore.models.resource_workflow_steps import SpatialCondorJobRWS
from tethysext.atcore.models.resource_workflow_steps.xms_tool_rws import XMSToolRWS
from tribs_adapter.workflows.tribs_workflow import TribsWorkflow
from tribs_adapter.workflows.utilities import get_condor_env
from tribs_adapter.workflow_steps.select_point_rws import SelectPointRWS


def validate_threshold_area(value):
if float(value) <= 0:
raise ValueError('Threshold area must be a positive number in square kilometers.')


class DelineateHydrologicFeaturesFromPointWorkflow(TribsWorkflow):
"""
Data model for storing information about delineate hydrologic features from point workflows.
"""
TYPE = 'delineate_hydrologic_features_from_point_workflow'
DISPLAY_TYPE_SINGULAR = 'Delineate Hydrologic Features From Point Workflow'
DISPLAY_TYPE_PLURAL = 'Delineate Hydrologic Features From Point Workflows'
REQUEST_CPUS_PER_JOB = 1

__mapper_args__ = {'polymorphic_identity': TYPE}

@classmethod
def new(cls, app, name, resource_id, creator_id, geoserver_name, map_manager, spatial_manager, **kwargs):
"""
Factor class method that creates a new workflow with steps
Args:
app(TethysApp): The TethysApp hosting this workflow (e.g. Agwa).
name(str): Name for this instance of the workflow.
resource_id(str|uuid): ID of the resource.
creator_id(str): Username of the user that created the workflow.
geoserver_name(str): Name of the SpatialDatasetServiceSetting pointing at the GeoServer to use for steps with MapViews.
map_manager(MapManagerBase): The MapManager to use for the steps with MapViews.
spatial_manager(SpatialManager): The SpatialManager to use for the steps with MapViews.
kwargs: additional arguments to use when configuring workflows.

Returns:
ResourceWorkflow: the new workflow.
""" # noqa:E501
condor_env = get_condor_env()

# Create new workflow instance
workflow = cls(name=name, resource_id=resource_id, creator_id=creator_id, lock_when_finished=True)

# Setup Condor Step
job_executables_dir = os.path.join(os.path.dirname(__file__), 'job_executables')

xmstool_step = XMSToolRWS(
name='tRIBS Watershed from Pour Point',
order=10,
help='Run the tRIBS Watershed from Pour Point tool from xmsool',
options={
'xmstool_class': 'tribs_adapter.workflows.delineate_hydrologic_features_from_point'
'.tools.WatershedFromPourPointWebTool',
'arg_mapping': {
'input_raster': {
'resource_attr': 'datasets',
'filter_attr': 'dataset_type',
'valid_values': [
'RASTER_DISC_ASCII', 'RASTER_CONT_ASCII', 'RASTER_DISC_GEOTIFF', 'RASTER_CONT_GEOTIFF'
],
'name_attr': 'name',
},
},
'form_title': 'tRIBS Watershed from Pour Point',
'renderer': 'django',
'validators': {
'threshold_area_sq_km': validate_threshold_area
}
}
)
workflow.steps.append(xmstool_step)

select_point_step = SelectPointRWS(
name="Select a Pour Point",
order=20,
help="Choose a pourpoint to delineate the watershed."
"The point should be located within the watershed of interest.",
geoserver_name=geoserver_name,
map_manager=map_manager,
spatial_manager=spatial_manager,
active_roles=[TribsRoles.ORG_USER, TribsRoles.ORG_ADMIN],
)
workflow.steps.append(select_point_step)

xmstool_job = {
'name': 'run_tool',
'condorpy_template_name': 'vanilla_transfer_files',
'remote_input_files': [os.path.join(job_executables_dir, 'run_tool.py'), ],
'attributes': {
'executable': 'run_tool.py',
'transfer_output_files': [],
'transfer_input_files': [],
'environment': condor_env,
'request_cpus': cls.REQUEST_CPUS_PER_JOB
},
'parents': [],
}

generate_datasets_step = SpatialCondorJobRWS(
name='Run Tool',
order=30,
help='Review input and then press the Run button to run the model. '
'Press Next after the model execution completes to continue.',
options={
'scheduler': app.SCHEDULER_NAME,
'jobs': [xmstool_job],
'working_message': 'Please wait for the tool to finish running before proceeding.',
'error_message':
'An error occurred with the run. Please adjust your input and try running '
'the tool again.',
'pending_message': 'Please run the tool to continue.'
},
geoserver_name=geoserver_name,
map_manager=map_manager,
spatial_manager=spatial_manager,
active_roles=[TribsRoles.ORG_USER, TribsRoles.ORG_ADMIN]
)
workflow.steps.append(generate_datasets_step)

return workflow
Loading
Loading