contents  prev top bottom  next

Chapter 3: Exercises and Examples     contents   ↑  ^   v   

3.0 Configuration     contents     ^   v   

3.0.1 Settings     contents    ^  ─  +   v   

By default initial setting, we mean the setting file that is used on startup in the situation where this had not been specifically defined. The initial setting can be specified by creating a user version of StartUpConfig.py where the item "defaultSettings" is set to the desired initial settings file. This can now also be done using SETUP->Manage Settings->Make My Current Default in the console.

NOTE: PUTTING A FILE IN THE DIRECTORY THAT StartUpConfig.py LIVES IN THAT IS NOT PRECISELY NAMED StartUpConfig.py WILL CAUSE A CATASTROPHIC FAILURE STARTING UP HAZARD SERVICES. (HOPEFULLY, THIS IS TEMPORARY BEHAVIOR).

(The new Hazard Services Overrides Installer available in the SCP has safeguards against this problem.)


Be aware that there is now a ‘universal generic base’ for all settings files, CommonSettings.py, found in /awips2/edex/data/utility/common_static/base/HazardServices/settings/config/. Also be aware that the way specific settings files interoperate with CommonSettings.py is still in flux.

It is possible to use the localization perspective to manipulate settings. However, for most things one might want to do with settings it makes more sense to use the Edit Setting: GUI (at SETUP->Manage Settings->Edit/Filter… in the console). See section 5.0 of the User Guide for more information about this. There are exceptions to this, including promoting custom settings to site level , configuring CommonSettings.py, and removing a base level default setting file.  For completeness, we will show some examples of using the localization perspective to manipulate settings.

3.0.1.1 Modifying an existing Settings file     contents    ^  ─  +   v   

   "visibleColumns": [
        "Event ID",
        "Lock Status",
        "Hazard Type",
        "Status",
        "Stream",
        "Point ID",
        "Start Time",
        "End Time",
        "Expiration Time",
        "VTEC Codes",
    ],

Hydrology_All = {
    "visibleColumns": [
        "Event ID",
        "Lock Status",
        "Hazard Type",
        "Status",
        "Point ID",
        "Start Time",
        "End Time",
        "Expiration Time",
        "VTEC Codes",
    ],
}

Now you can restart Hazard Services and verify that the Stream column is gone, right? Wait a minute, the Stream column is still there...what happened? This is a result of the fact that this file is subject to incremental override. Incremental override is why one does not have to copy the whole base file to the site file. However, for incremental override, the default behavior for two lists at the same namespace is to merge the lists rather than to replace the list in the base file with the list in the site file. You have two options in the site file that can change this default behavior, as follows:

     "visibleColumns": [
        "_override_replace_",
        "Event ID",
        "Hazard Type",
        "Status",
        "Point ID",
        "Start Time",
        "End Time",
        "Expiration Time",
        "VTEC Codes",
    ]
    "visibleColumns": [
        "_override_remove_list_",
        "Stream",
    ],

 

3.0.1.2 Creating a new Settings file   contents    ^    +  v   


This can be done in two ways; through the
Edit Settings: dialog or through the localization perspective. Using the Edit Settings: dialog has the advantage of it being very hard to shoot yourself in the foot. Using the localization perspective has the advantage that the resulting settings file can be made ‘incremental’ (e.g. you only define the things you want to change.)

Here is how this can be done through the Edit Settings: dialog.


Here is how this can be done through the localization perspective:

3.0.1.3 Promoting a settings file to Site level.   contents    ^    +  v   

It is perfectly reasonable to use the dialog to initially create a new settings file, and then to later go into the localization perspective to further modify the settings information. Also, be advised that both of these means for creating a new settings file will create a USER level settings file. If it is desired to make the new settings file a SITE level settings file, this can be accomplished through the localization perspective as follows:

3.0.1.4 Console Time Window.   contents    ^    +  v   

A recently added feature is to allow a setting file to specify the range of hours offset from the current time for which to display hazards in the Console. This currently defaults to essentially an infinite time range in all default settings files. Some sites or users may find it more convenient now for default settings files to make ALL Status values visible and apply a reduced size console time window. What follows is an override for CommonSettings.py that can accomplish this.

CommonSettings = {
   "consoleTimeWindow": ("_override_replace_", -8, 999),
   "visibleStatuses": [
        "ended",
        "elapsed"
    ],
}

With this example override file, hazards valid in the future will appear in the Console essentially forever, but previous hazards will appear in the Console only for the last 8 hours (e.g. one shift). If there is a desire to have this apply to all users, installing this as a Region level override is a reasonable way to do this.

Note that the Console Time Window can be controlled using the Console tab of the Edit Settings: dialog.

3.0.1.5 Remove default base setting file.   contents    ^    +  v   

New settings files created by the user that are not overrides of an existing default base setting file are straightforward to remove.  If they are implemented at the User level, this can be done either using the localization perspective or by way of SETUP->Manage Settings->Delete.  At the Site level, one can still use the localization perspective, but at the Configured or Region level this needs to be done using the file system.  However, consider the situation where one wants to remove a settings file that was delivered as Base level default settings file.  This cannot be done by overriding or undoing an override of the setting file itself; this must be done using an override of StartUpConfig.py.  Suppose one wanted to remove the Hydrology_ESF settings file.  Here is an override of StartUpConfig.py that can accomplish this:

StartUpConfig = {
        "turnOffFeatures_practiceMode":[
             {
             "settings": ["Hydrology_ESF"],
             "recommenders": [],
             }
             ],
        "turnOffFeatures_testMode" : [
             {
             "settings": ["Hydrology_ESF"],
             "recommenders": [],
             }
             ],
        "turnOffFeatures_operationalMode" : [
             {
             "settings": ["Hydrology_ESF"],
             "recommenders": [],
             }
             ],
}


CAUTION!!! It is very important that one provide a list (can be empty) of both “settings” and “recommenders” in each turn off set, even if one is not removing any items of that type. Failure to do so will result in a catastrophic failure starting Hazard Services.

3.0.2 StartUpConfig       contents    ^    +  v   

NOTE: PUTTING A FILE IN THE DIRECTORY THAT StartUpConfig.py LIVES IN THAT IS NOT PRECISELY NAMED StartUpConfig.py WILL CAUSE A CATASTROPHIC FAILURE STARTING UP HAZARD SERVICES. (HOPEFULLY, THIS IS TEMPORARY BEHAVIOR).

(The new Hazard Services Overrides Installer available in the SCP has safeguards against this problem.)

Here we show how to modify an entry in StartUpConfig.py. Note that StartUpConfig.py is subject to incremental override, and so one only need specify the items to change; there is no need to copy the whole base file.

StartUpConfig = {
    "defaultSettings" : "Hydrology_All",
}

Note that the console control SETUP->Manage Settings->Make My Current Default is just like entering your current setting as the default setting for an override of StartUpConfig.py. Also note that this operation results in an incremental version of StartUpConfig.py being written out.

3.0.3 Hazard Types       contents    ^  ─  +  v   

3.0.3.1 Modifying a Hazard Type entry     contents    ^  ─  +  v   

Here we show how to modify a Hazard Type entry. These entries occur in the file HazardTypes.py. Note that HazardTypes.py is subject to incremental override, and so one only need specify the items to change; there is no need to copy the whole base file.

HOURS = 3600000
HazardTypes = {
    "FA.A": {
         'defaultDuration': 6 * HOURS,
    },
}

Here is a related example for a hazard type that has a list of durations to pick from. Suppose that it is desired to allow Flash Flood warnings of a shorter duration (this is often a convenient way to test the behavior of hazards as they approach their end time). Here are two possibilities for what the content of the override file could look like in that case:

HazardTypes = {
  'FF.W.Convective' : {
         'durationChoices' :
           [ "_override_prepend_",
             "15 min", "30 min", "45 min" ]
  }
}
HazardTypes = {
  'FF.W.Convective' : {
         'durationChoices':
           ["_override_replace_",
            "15 min", "30 min", "45 min",
            "60 min", "90 min", "120 min",
            "2 hrs 30 min", "3 hrs",
            "3 hrs 30 min", "4 hrs",
            "4 hrs 30 min", "5 hrs",
            "5 hrs 30 min", "6 hrs",
            "6 hrs 30 min", "7 hrs",
            "7 hrs 30 min", "8 hrs",
             ]
  }
}

Because HazardTypes.py is subject to incremental override, the default behavior for two lists at the same namespace is to merge the lists rather than to replace the list in the base file with the list in the site file. The first example override file tells the system to prepend  '15 min',  

‘30 min’, and  '45 min' to the list of durations, whereas the second example starts by stipulating that the supplied override list should replace the base list.

3.0.3.2 Completely removing an existing Hazard Type.     contents    ^   +  v   

In this section we describe how to completely remove an existing default hazard type from Hazard Services. We make the assumption that if one wants to remove a user defined hazard type from Hazard Services, one can simply undo the steps described in section 4.2 which were used to add that hazard type in the first place.

In the section entitled disabling the ability to issue a hazard type, we describe how to make a hazard type unissuable. Making a hazard type unissuable retains the ability to view hazards of that type from neighboring offices and the ability to manage that hazard type in the settings. The changes documented in this section result in eliminating all ability for Hazard Services to interoperate with hazards of that type. This means hazards of that type from neighboring offices cannot be viewed, nor can this hazard type be managed in the settings.

Here we will demonstrate specifically how to eliminate the Flood Advisory (FL.Y) hazard type.
We will start by eliminating it from HazardCategories.py. The approach is to create a site override for HazardCategories.py containing the following:

from collections import OrderedDict

HazardCategories = OrderedDict(
        {
        "Hydrology": ["_override_remove_", ("FL", "Y"), ],
        }
)


HazardCategories.py is subject to
incremental override, and so there is no need to replicate the whole file, we just need to mark FL.Y for deletion from its category, "Hydrology". Were an override for HazardCategories.py already present, its new content would need to be a merge of the immediately preceding example and its current content. If an override file for HazardCategories.py had been used to reassign FL.Y to a different category, updating the override file would need to include removing FL.Y from its new category as well.

Hazard Service’s decisions about which hazard types are important to the console are completely driven by the content of your current setting, and the code that manages the settings files is smart enough to completely throw on the floor any hazard type that does not have a category.  Functionally removing a hazard type from Hazard Services does not require removing the main entry for that hazard type from HazardTypes.py. In fact, attempting to do this would likely cause more problems than it would solve and therefore is not recommended.

This does not mean that no changes at all are necessary in HazardTypes.py. Once a hazard type has been ‘orphaned’ from all categories, one needs to remove all occurrences of that hazard type from 'replacedBy': lists.  This means an override for HazardTypes.py needs to be created where there is an entry for each hazard type for which the hazard type being removed appears in the base version’s 'replacedBy': list.  Again using FL.Y as an example, such an override would contain the following:

HazardTypes = {
    'FL.A' : { 'replacedBy': ["_override_remove_list_", 'FL.Y'] },
    'FL.W' : { 'replacedBy': ["_override_remove_list_", 'FL.Y'] },
    'HY.S' : { 'replacedBy': ["_override_remove_list_", 'FL.Y'] },
}

(In 20.2.1, work against #75201 will eventually mean the updates for the ‘replacedBy’ lists will be unnecessary.) Since a single hazard is being removed from 'replacedBy' in these instances, one could technically use _override_remove_ instead of _override_remove_list_.  However,  _override_remove_ only removes one item, and having _override_remove_list_ in place sets you up to also remove any additional items you might add to the end of list, making it harder to overlook this.

 As mentioned, it is true that removing a hazard type from all categories functionally removes that hazard type from the console. The same cannot be said, however, for any recommenders that might generate hazards of that type. Nor is this true for the logic that displays hazards.


For the sake of argument, let us stipulate (not true) that the RiverFloodRecommender recommends hazards
only of type FL.Y and no other types. Therefore, all references to that recommender need to be removed from all our settings files. Plus, we want to remove all references to “FL.Y” from the settings files. These four commands allow one to easily identify any setting files for which this is an issue:

> touch /tmp/bbb

> cd /awips2/edex/data/utility/common_static

> find . -name '*.py' -path '*/settings/*' -exec grep 'RiverFloodRecommender' '{}' /tmp/bbb \;

> find . -name '*.py' -path '*/settings/*' -exec grep 'FL.Y' '{}' /tmp/bbb \;

For any base settings file identified where there is no corresponding site override, one needs to create a site override containing what follows. The italicized text is not literal, but rather is the name of the settings file minus the .py extension. Please note that while we are using the file system to identify those settings files that we need to change, creating and/or updating the site override file should be done in the localization perspective.

Settings_File_Name = {
   "visibleTypes": [ "_override_remove_", "FL.Y" ],
   "toolbarToolNames": [
       "_override_remove_",
       "RiverFloodRecommender"
   ]
}

For any override settings file where there is no corresponding base file, one simply needs to remove any instance of "RiverFloodRecommender" from the "toolbarToolNames" , plus any occurrence of “FL.Y”. The same goes for any override settings file where there is an entry of “_override_replace_”: True, at the top level. Otherwise the modified override settings file needs to be a merge of the immediately preceding example and its current content.


In addition to removing a default recommender from all settings, one can in theory also completely remove it from the system, which needs to be done by way of StartUpConfig.py. See
section 3.0.1.5 for more information.

As previously mentioned, it is not actually true that the RiverFloodRecommender recommends hazards only of type FL.Y. Therefore, if one were actually completely removing the hazard type FL.Y from Hazard Services, one needs to go into the recommender itself and disable its ability to interoperate with hazards of type FL.Y. For this particular recommender, this is detailed in section 3.3.3. For other recommenders that recommend multiple hazard types, the details would be different. It is also possible for a hazard type to have no associated recommender. In that case, none of the override settings files would need any entries for "toolbarToolNames".

3.0.4 FilterIcons.py     contents    ^   +  v   

The purpose of FilterIcons.py is to allow icons to be placed on the left side of the console tool bar that indicate when hazard events meeting certain criteria have been filtered out. One can see the default version of FilterIcons.py in the localization perspective under Hazard Services -> Hazard Categories. What follows is a sample override for this file. This file is subject to incremental override, and so to eliminate the default entry the list of entries begins with '_override_replace_' . If there was a desire to only add new entries, those entries could be placed in the override file and the default entry would still be in effect as well. The image referred to with the 'normalIcon' element is what shows up when no events meeting the criteria have been filtered out.; the 'coloredIcon' element indicates the image shown when there ARE events being filtered out that meet the criteria.

FilterIcons = [
    '_override_replace_',
    {
    'label': 'Hydrology Watches',
    'normalIcon': 'waveIcon.png',
    'coloredIcon': 'waveIconYellow.png',
    'hazardTypes': [
                    'FF.A', 'FA.A',],
    'status': ['potential', 'pending', 'issued', 'ending']
    },
    {'label': 'Hydrology Warnings',
    'normalIcon': 'unlocked.png',
    'coloredIcon': 'locked.png',
    'hazardTypes': ['FF.W.Convective', 'FF.W.NonConvective', 'FF.W.Burnscar',
                    'FA.W','FL.W'],
    'status': ['potential', 'pending', 'issued', 'ending']
    },
    {'label': 'Hydrology Advisory',
    'normalIcon': 'MultiValueScaleNormalThumbImage.png',
    'coloredIcon': 'MultiValueScaleActiveThumbImage.png',
    'hazardTypes': [
                    'FA.Y', 'FL.Y',],
    'status': ['potential', 'pending', 'issued', 'ending']
    },
]


3.0.5 Highway Mile Markers in Flash Flood Warnings     contents    ^   v   

Note that it was recently discovered that certain implementations of mileMarkers.xml and mileMarkers.vm can result in problematic content for PointMarkerConfigs.py after running parseWarngenTemplate.py with the -m option.  This can manifest as route entries with an empty string for the “type”: element.  If there are any problems with the mile marker functionality, first download the most recent version of the parseWarngenTemplate.py script (see  section HH)  and try running parseWarngenTemplate.py again.

Hazard Services by default supports the ability to list the mile markers of impacted highways in flash flood warnings. Hazard Services leverages much of the same scripting and data structures to do this that warnGen does. For both Hazard Services and warnGen, this functionality is dependent on creating a table in postgres for each route.   These tables appear in the mapdata schema of the maps database.  For warnGen, the name of each table that supports the mile marker functionality is placed in an xml file; for Hazard Services, each table needs to be inserted into an override for the file PointMarkerConfigs.py.  

Here is an example of an entry in PointMarkerConfigs.py that supports the mile marker functionality for one route:

PointMarkerConfigs = {
   "mileMarkers": {
        "tables": [
            {
                "displayName": "Interstate 29 in Nebraska",
                "tableName": "i29_oax",
                "returnFields": [
                    "id",
                    "name"
                ],
                "maxResults": 10,
                "simplify": True,
                "sortBy": [
                    "gid",
                    "asc"
                ],
                "type": "mile marker"
            },
        ],
    },
}

Note that the content in blue is the structure within which the content for each route exists, the content in black is what is specific to the route for this example.  Also note that the site ‘oax’ is embedded in the table name entry. Unlike dam break or burn scar information, there is no formal segregation in postgres according to CWA for this information.  Therefore it is a good idea to always embed the CWA name that the route exists within as part of the table name.

For now lets assume the mile marker functionality is already working in warnGen, and has been implemented for all the markers along all the routes you care about at your site.  If this is the case, there should already be a postgres table for each route, and so one only need create an override for PointMarkerConfigs.py to leverage those.  A python script has been updated to do most of this work for you.  To invoke this script, become user awips on dx3, and run these commands:

> cd /awips2/edex/scripts/HazardServices/

> /awips2/python/bin/python parseWarngenTemplate.py -m

With the -m option, parseWarngenTemplate.py reads the files mileMarkers.vm and mileMarkers.xml from site/LLL/warngen/ and outputs an override for PointMarkerConfigs.py in the following directory (LLL is not literal, but is your primary site):

common_static/configured/LLL/HazardServices/python/events/productgen/geoSpatial
/

You can run parseWarngenTemplate.py with no arguments to get usage information.  You can also supply a backup site id on the command line to run it for any arbitrary backup site. Note that if this conversion step fails to create a useable override of PointMarkerConfigs.py, it is always possible to compose these entries for yourself, following the example shown.  If for some reason you want to add mile markers for an additional route not yet implemented in your warnGen, that will also require composing one or more of these entries.

If after running the default conversion, the mile marker functionality in hazard services is working without any problems, then it would be reasonable to promote the configured level of PointMarkerConfigs.py to the site level and remove the configured level.  The only reason to keep the configured level file around would be if you think that you may need to rerun the conversion for some reason.

If you decide to have both a configured and site version of PointMarkerConfigs.py, there are some things to be aware of.  The set of route table entries is a list, and so the default behavior is to add the entries in the site version to those in the configured version.  If adding new route entries is all you want to do with the site override, then the default behavior will be fine.  If in any way you wish to use a site override to “fix” something in the configured override, you will need to use control strings in the site override, following one of these approaches:

PointMarkerConfigs = {
   "mileMarkers": {
        "tables": [
            "_override_replace_",
            replicate all
            route entries
        ],
    },
}
PointMarkerConfigs = {
   "mileMarkers": {
        "tables": [
            "_override_by_key_tableName_",
            route entries to
            add/modify
        ],
    },
}
PointMarkerConfigs = {
   "mileMarkers": {
        "tables": [
            "_override_by_key_displayName_",
            route entries to
            add/modify
        ],
    },
}

Note that the italicized text is not literal. For the first approach, it is as if the configured version is not even there, except that it provides you with a record of the raw conversion. The other two approaches allow you to apply incremental overrides to the individual route table entries, using either the “tableName” or “displayName” value to define the namespace.

In the case where the mile marker functionality is already working in warnGen, all the postgres tables referred to by the version of PointMarkerConfigs.py created here should already be in postgres.  If mile markers for some or all of the routes are not showing up in FFW text, this means that some or all of the postgres tables are not installed.  If this is the case, here is what to do.  First, identify all the tables referred to in all your overrides of PointMarkerConfigs.py using these commands, best run as user awips on dx3:

> cd /awips2/edex/data/utility/common_static

> grep tableName */LLL/HazardServices/python/events/productgen/geoSpatial/PointMarkerConfigs.py


Next, identify all the tables in the
mapdata schema of the maps database.  The command that follows will do this (leave off the -U awips -hdx1f in a dev environment):

> psql -U awips -hdx1f -d maps -c '\dt mapdata.*'

For any table referred to in an override of PointMarkerConfigs.py that is not in postgres, one either needs to remove all entries that refer to it in any overrides of PointMarkerConfigs.py, or add it to postgres.  To add such a table to postgres, one first needs a file that contains a list of mile markers for a single route in a very specific format.  What follows is an example of such a file.  The name of the file does not matter except that it helps keep this information organized in a meaningful way for the person managing this.


highway92_oax.id :

7
1        41.192        -97.354        p        1        409|1
2        41.192        -97.296        p        2        412|1
3        41.192        -97.238        p        3        415|1
4        41.192        -97.162        p        4        419|1
5        41.206        -97.096        p        5        423|1
6        41.206        -97.057        p        6        425|1
7        41.206        -97.000        p        7        428|1


The script used to install this information as a postgres table is available only on dx1, and should be run as user awips. The full path to this script is: /awips2/database/sqlScripts/share/sql/maps/importMarkersInfo.sh

The importMarkersInfo.sh script is very particular about this format, so it is recommended to not deviate from it. If you have already successfully installed mile markers for warnGen, then you will likely already have a set of these ID files that you can reuse for creating mile marker postgres tables for Hazard Services if necessary. The first line in the ID file always contains the number of entries in the file. The ‘p’ and the ‘|1’ are literal and should always be placed in each line exactly as shown. The first and fifth whitespace delimited columns are sequence numbers and should always count up. Just before the vertical bar are the mile marker values; these are arbitrary strings. The latitude and longitude values are self explanatory.

Here is how this script is run to create a postgres table:

> importMarkersInfo.sh /path/to/markerfile.id mapdata tableNameInPointMarkerConfigs.py

mapdata is literal. This results in a new table being created in the mapdata schema of the maps database with the same name as the final argument. Unlike dam break or burn scar information, there is no formal segregation in postgres according to CWA for this information.  Therefore it is a good idea to always embed the CWA name that the route exists within as part of the table name.

The very first time importMarkersInfo.sh is called for a given table name, you might see a message like:
    NOTICE:
 table "highway92_oax" does not exist, skipping
This seems bad, but does not necessarily mean the script failed. It may mean simply that it ‘skipped’ the step of deleting the table before reinstalling it.

3.0.6 Other arbitrary Markers in Flash Flood Warnings.     contents    ^   v   

Hazard Services implements the ability to mention when the location of other arbitrary markers occur within the bounds of a Flash Flood Warning. Here we will present an idealized example of this. It starts with a site override of PointMarkerConfigs.py:

PointMarkerConfigs = {
                       "publicevents" : {
                           "tableName" : "publicevents",
                           "returnFields" : ["name"],
                           "pointSpecificLabel" : "Public Events"
                           },
                 }

With this in place, the Additional Locations: section of the Hazard Information Dialog for a Flash Flood Warning will have a checkbox labeled "Public Events" (e.g., the value of the “pointSpecificLabel" key). In order for this to have useful functionality, the importMarkersInfo.sh script must be used to create a new table in the mapdata schema of the maps database with the name "publicevents" (e.g. the value of the "tableName" key). Suppose the following invocation of importMarkersInfo.sh was used:
> importMarkersInfo.sh /tmp/publicEvents.id mapdata publicevents


Furthermore, suppose that the contents of publicEvents.id were the following:

2
1  41.22  -96.11 p 1 Little League Finals|1
2  41.33  -96.27 p 1 Regional Tractor Pull|1

Finally, suppose that one created an FFW where the warning box contained both of those latitude/longitude points and the "Public Events" checkbox was activated in the Hazard Information Dialog. The resulting text product would contain the following small paragraph:


This includes the following Public Events...

        Little League Finals and Regional Tractor Pull.

3.1 Recommenders     contents    ^   +  v   

Please see this presentation to become familiar with the Recommender Framework. Also, please see the section on how to configure a Recommender Dialog using megawidgets.

3.1.1 Modifying an existing Recommender     contents    ^  v   

class Recommender(RecommenderTemplate.Recommender)
    def getWarningThreshold(self):
        return 20 

3.1.2 Interdependencies in a Recommender.     contents    ^  v   

Just as with metadata, it is possible for megawidgets associated with a recommender to be subject to an interdependency script. Here is an example of a recommender customization that makes use of an interdependency script. What this does is add one or more extra buttons to the River Flood Recommender GUI that activate any arbitrary list of gauges you want. These can be individual gauges or a set of river groups (hence the terminology “super group”). For any specific WFO, the logic in assignSuperGroups() would need to be customized for that WFO; the rest of the logic should work fine exactly as is. This example is for WFO OAX, which has rivers in Nebraska, rivers in Iowa, and the Missouri River which flows along the border. Note that the super group buttons can only turn on gauges, not turn them off. It would be possible to update this logic to be able to turn off super group gauges, but that would require work beyond modifying the assignSuperGroups() method. There are some commented out diagnostics in this code example; those could be uncommented to help understand what is going on if difficulties were encountered implementing this.

class Recommender(RecommenderTemplate.Recommender):
   
    # Return a dictionary of super group names that maps to a list of
    # gauges or groups that will get activated if that button is hit.
    def assignSuperGroups(self, riverChoices) :

        # Keys here are what actually appear on the menu buttons associated
        # with each super group.
        superGroups = {"Iowa Rivers": [], "Nebraska Rivers": [],
                       "Missouri River": []}

        # Spin through each river group known to recommender. List of ids
        # associated with each super group can be either identifiers of
        # river groups or identifiers of individual gauges (e.g. children).
        for riverChoice in riverChoices :
            if "Missouri" in riverChoice["displayString"] :
                sgName = "Missouri River"
            elif "I4" in riverChoice["children"][0] :
                sgName = "Iowa Rivers"
            else :
                sgName = "Nebraska Rivers"
            superGroups[sgName].append(riverChoice["identifier"])
        return superGroups

    def addForecastPointChoicesDict(self, fieldDicts, valueDict):
        '''
        # Create hierarchical list of rivers and forecast points to choose from
        '''
        choices = self.riverChoices()
        if not choices:
            return
        superGroups = self.assignSuperGroups(choices)
        import json
        # ffff = open("/tmp/superGroups.txt", "w")
        # ffff.write(json.dumps(superGroups, indent=4)+"\n")

        # Process each super group to create a button for it.  The list of identifiers
        # for gauges/river groups associated with the super group is added to the
        # button by way of the 'extraData'.
        for superGroup in superGroups :
            buttonid = superGroup.replace(" ","")+"Button"
            fieldDicts.append({'fieldType': "Button",
                               'fieldName': buttonid,
                               'label': superGroup,
                               'extraData': {"choices": superGroups[superGroup]}  })
        # ffff.write(json.dumps(fieldDicts, indent=4)+"\n")
        # ffff.close()

        forecastPointChoicesDict = {
                'fieldType': 'HierarchicalChoicesTree',
                'fieldName': 'forecastPointChoices',
                'label': '',
                'lines': 8,
                'expandHorizontally': True,
                'expandVertically': True,
                'allowScrolling': True,
                'choices': choices,
                }
        forecastPointChoicesGroup = {
            "fieldName": forecastPointChoicesDict.get('fieldName') + "Group",
            "fieldType":"Group",
            "label": "Forecast Point Choices",
            "expandHorizontally": True,
            "expandVertically": True,
            "fields": [forecastPointChoicesDict]
        }
        fieldDicts.append(forecastPointChoicesGroup)
        choiceValues = []
        for riverDict in choices:
            values = {
                      'displayString': riverDict['displayString'],
                      'identifier': riverDict['identifier'],
                      'children': riverDict['children']
                      }
            choiceValues.append(values)
        valueDict['forecastPointChoices'] = choiceValues


def applyInterdependencies(triggerIdentifiers, mutableProperties):
    import json
    # ffff = open("/tmp/applyInterdependencies.txt", "w")
    # ffff.write(str(triggerIdentifiers)+"\n")
    propertyChanges = None
    if not triggerIdentifiers :
        return propertyChanges

    # Only when there is exactly 1 triggerIdentifier does this represent a user choice
    # in the recommender GUI. Also, we only want to respond to activity on a Button.
    if len(triggerIdentifiers)!=1 or not "Button" in triggerIdentifiers[0]:
        # ffff.close()
        return propertyChanges

    # Gather up information we need to respond to super group activation.  sgshoices is
    # the list of ids associated with the super group. choices is the entire set of
    # gauge choices that could one could make in the GUI, and values is the current set of
    # gauges that have actually been chosen.
    try :
        sgchoices = mutableProperties[triggerIdentifiers[0]]["extraData"]["choices"]
        choices = mutableProperties["forecastPointChoices"]["choices"]
        values = copy.deepcopy(mutableProperties["forecastPointChoices"]["values"])
    except :
        # ffff.close()
        return propertyChanges

    # ffff.write(str(sgchoices)+"\n")
    # ffff.write(json.dumps(choices, indent=4)+"\n")
    # ffff.write(json.dumps(values, indent=4)+"\n")

    # Process each gauge/river group is associated with super group we are activating.
    any = False
    for sgchoice in sgchoices:

        # From the entire set of gauge choices, figure out which river group sgchoice refers to.
        matchchoice = None
        for choice in choices :
            if sgchoice in choice["children"] or sgchoice==choice["identifier"] :
                matchchoice = choice
                break
        if not matchchoice :
            continue

        # From the current set of gauge choices, figure out which river group sgchoice refers to,
        # if any.
        matchvalue = None
        for value in values :
            if value["identifier"]==matchchoice["identifier"] :
                matchvalue = value
                break

        if not matchvalue :
            # Set of current gauge choices does not yet refer to the river group
            # that the super group id refers to, so copy from possible choices.
            # Set single selected gauge if so indicated, by - in the id.
            matchvalue = copy.deepcopy(matchchoice)
            if "-" in sgchoice :
                matchvalue["children"] = [sgchoice]
            values.append(matchvalue)
        elif "-" in sgchoice :
            # Single gauge to add to group already in current choices, verify it has
            # not already been activated.
            if sgchoice in matchvalue["children"] :
                continue
            matchvalue["children"].append(sgchoice)
        else :
            # Attempt to activate entire group, first verify entire group has not already
            # been activated.
            if len(matchvalue["children"])>=len(matchchoice["children"]) :
                continue
            matchvalue["children"] = copy.deepcopy(matchchoice["children"])

        # Note that something has been successfully updated.
        any = True

    # If something has been changed, pass updated set of "values" back.
    if any :
        propertyChanges = { "forecastPointChoices" : { "values" : values} }

    # ffff.write("propertyChanges:\n")
    # ffff.write(json.dumps(propertyChanges, indent=4)+"\n")
    # ffff.close()
    return propertyChanges

 

3.1.3 Creating a new Recommender     contents    ^  v   


Consider a case where a particular dam was at imminent risk of failing and producing catastrophic impacts. Here we demonstrate how one can add a new recommender for the purpose of having a recommendation for that specific dam ready to go on the very top of the
Tools menu.  For this example, we are using a dam called “Old River Dam”, which is an actual dam that was configured for warnGen in a sample damInfo.vm and damInfoBullet.xml we were given from a WFO for testing.  First thing is to create an instance of the new recommender. Go into the localization perspective to:
    
Hazard Services->Recommenders->DamLeveFlood.py->BASE
Using the third button popup on that selector, choose
Copy To->New File… In the dialog that comes up entitled ‘Rename File’, type in the name of the new recommender, which for this example is OldRiverSpecial.py, and hit OK. What follows is the content of OldRiverSpecial.py for this example.  Of course, if you were doing this for one of your own dams, the name of the recommender and some of these changes that follow would be different. Also, this creates a user level override. Once these changes were tested, they would need to be promoted to site level overrides if the new behavior was something that was meant to support operations. We show the differences from the default dam break recommender, DamLeveeFlood.py, in the customary green.

OldRiverSpecial.py:

import datetime
import collections
import logging
import RecommenderTemplate
import numpy
import GeometryFactory
import JUtil
import UFStatusHandler
import EventFactory
import EventSetFactory
import GeneralUtilities
from MapsDatabaseAccessor import MapsDatabaseAccessor
from shapely.prepared import prep
import HazardConstants
import GeneralConstants
from EventSet import EventSet
from HazardEventLockUtils import HazardEventLockUtils

RESULTS_OUTPUT_KEY = "resultsOutput"

class Recommender(RecommenderTemplate.Recommender):

    def __init__(self):
        self.DEFAULT_FFW_DURATION_IN_MS = 10800000
       
        self.HAZARD_TYPE_MAP = {
                                "W": "Warning",
                                "A": "Watch"
                                }

        self.NO_MAP_DATA_ERROR_MSG = '''No mapdata found for Dam/Levee Flood inundation.
See the alertviz log for more information.
(Please verify your maps database contains the table mapdata.{})

Please click CANCEL and manually draw an inundation area.
 '''.format(HazardConstants.AREA_TABLE)
       
        self.LOCKED_HAZARD_RESULTS_OUTPUT = '''The {} already has a {} Event: {} that is {} and locked.
Unable to make new recommendations at this time.'''

        self.HAZARD_EXISTS_RESULTS_OUTPUT = '''The {} already has a {} Event: {} that is issued.
Unable to make new recommendations at this time.'''

        self.MISSING_HAZARD_RESULTS_OUTPUT = '''No event for {} could be created.
Unable to make new recommendations at this time.'''

        self.HAZARD_INVALID_GEOMETRY_RESULTS_OUTPUT = '''The geometry for {} is missing or invalid.
Unable to make new recommendations at this time.'''

        self.logger = logging.getLogger('OldRiverSpecial')
        for handler in self.logger.handlers:
            self.logger.removeHandler(handler)
        self.logger.addHandler(UFStatusHandler.UFStatusHandler(
            'gov.noaa.gsd.common.utilities', 'OldRiverSpecial', level=logging.INFO))
        self.logger.setLevel(logging.INFO)
        self.hazardEventLockUtils = None
        self.mapsAccessor = MapsDatabaseAccessor()
        # Main key for the default instance of this dam in the main Dam/Levee Flood recommender.
        self.origKey_forDam = "Old River Dam"

    def defineScriptMetadata(self):
        """
        @return: JSON string containing information about this
                 tool
        """
        metaDict = {}
        metaDict["toolName"] = "OldRiverSpecial"
        metaDict["author"] = "GSD"
        metaDict["version"] = "1.0"
        metaDict["description"] = "Calculates inundation areas based on dams."
        metaDict["eventState"] = "Pending"
        return metaDict

    def defineDialog(self, eventSet, **kwargs):
        """
        @return: MegaWidget dialog definition to solicit user input before running tool
        @param eventSet: A set of event objects that the user can use to help determine
        new objects to return
        @param kwargs: Additional key word arguments to this method.
        """        
        jMethodInput = kwargs.get("methodInput", None)
        siteID = None
        if jMethodInput is not None:
            siteID = JUtil.javaMapToPyDict(jMethodInput).get('siteID', None)
       
        dialogDict = {"title": "Old River Emergency"}
         
        # Not needed because we already know which dam this is for. Just to show the
        # kinds of logic being removed from default dam break recommender. Other blocks
        # have been removed instead of being commented out.
        #damFieldDict = {}
        #damFieldDict["fieldName"] = "damOrLeveeName"
        #damFieldDict["values"] = "Please Select a Dam or Levee"
        #damFieldDict["fieldType"] = "ComboBox"
        #damFieldDict["autocomplete"] = True
        #damFieldDict["readonly"] = False

        urgencyFieldDict = {}
        urgencyFieldDict["fieldName"] = "urgencyLevel"
        urgencyFieldDict["label"] = "Please Select Level of Urgency"
        urgencyFieldDict["fieldType"] = "RadioButtons"
       
        # Leave out choice of "WATCH (Potential Structure Failure)"
        urgencyList = ["WARNING (Structure has Failed / Structure Failure Imminent)"]
        urgencyFieldDict["choices"] = urgencyList
       
        fieldDicts = [urgencyFieldDict]
        dialogDict["fields"] = fieldDicts
       
        valueDict = {"urgencyLevel":urgencyList[0]}
        dialogDict["valueDict"] = valueDict

        return dialogDict

    def defineResultsDialog(self, eventSet, **kwargs):
        """
        @return: A dialog definition to display recommender results
        """
        dialogDict = {}
        resultString = eventSet.getAttribute(RESULTS_OUTPUT_KEY)
        if resultString is None:
            return {}
        resultString += self.createResultsStringFromEventSet(eventSet)
        dialogDict["title"] = "Old River Emergency Results"
        widget = {
                  "fieldType": "Label",
                  "fieldName": "resultsLabel",
                  "values": resultString,
                }
        dialogDict["fields"] = [widget]
        dialogDict["minInitialWidth"] = 450
        return dialogDict

    def execute(self, eventSet, dialogInputMap, spatialInputMap):
        """
        @eventSet: List of objects that was converted from Java IEvent objects
        @param dialogInputMap: A map containing user selections from the dialog
        created by the defineDialog() routine
        @param spatialInputMap: A map containing spatial input as created by the
        definedSpatialInfo() routine
        @return: List of objects that will be later converted to Java IEvent
        objects
        """
        damOrLeveeName = "Old River Dam Special"

        if damOrLeveeName is None:
            return None
       
        urgencyLevel = dialogInputMap["urgencyLevel"]
        sessionAttributes = eventSet.getAttributes()
        siteID = dialogInputMap['siteID']

        self.damPolygonDict = {}
        try:
            self.damPolygonDict = self.mapsAccessor.getPolygonDict(HazardConstants.DAMINUNDATION_TYPE, siteID)
        except:
            self.logger.exception("Could not retrieve dam impact areas.")
            return None
       
        # Text to be displayed in the Results dialog
        resultsText = "Tool ran for {}:\n".format(damOrLeveeName)

        if self.hazardEventLockUtils is None:
            practice = GeneralUtilities.isPractice(sessionAttributes.get('runMode'))
            self.hazardEventLockUtils = HazardEventLockUtils(practice)

        lockedHazardIds = self.hazardEventLockUtils.getLockedEvents()

        newEventSet = EventSetFactory.createEventSet()
        hazardEvent = None
        for currentEvent in eventSet:
            locked = currentEvent.getEventID() in lockedHazardIds
            # Test against both our special key and the existing key, so that a watch for this dam done
            # on behalf of the default recommender will interoperate properly with a warning from this
            # recommender.
            if currentEvent.getHazardAttributes().get("damOrLeveeName") in [damOrLeveeName, self.origKey_forDam]:
                currentHazardType = self.HAZARD_TYPE_MAP.get(currentEvent.getSignificance())
                currentEventID = currentEvent.getDisplayEventID()
                status = currentEvent.getStatus()
                if status == "ISSUED":
                    if ("WARNING" in urgencyLevel and currentEvent.getSignificance() != "W") or \
                       ("WATCH" in urgencyLevel and currentEvent.getSignificance() != "A"):
                        if not locked:
                            currentEvent.setStatus('ending')
                            currentEvent.addHazardAttribute('keepSelected', True)
                            newEventSet.add(currentEvent)
                            break
                        else:
                            # Current Watch or Warning is Locked, don't recommend anything
                            # Update the resultsText to indicate this
                            resultsText += self.LOCKED_HAZARD_RESULTS_OUTPUT.format(damOrLeveeName, currentHazardType, currentEventID, status.lower())
                            newEventSet.addAttribute(RESULTS_OUTPUT_KEY, resultsText)
                            return newEventSet
                    else:
                        # Current event and urgency level match, return empty eventSet
                        # Update the resultsText to indicate this
                        resultsText += self.HAZARD_EXISTS_RESULTS_OUTPUT.format(damOrLeveeName, currentHazardType, currentEventID)
                        newEventSet.addAttribute(RESULTS_OUTPUT_KEY, resultsText)
                        return newEventSet
                elif status not in ['ENDED', 'ELAPSED']:
                    if not locked:
                        # Current Event is not issue, update it.
                        hazardEvent = currentEvent
                    else:
                        # Current Event is locked do not create a new event
                        # Update the resultsText to indicate this
                        resultsText += self.LOCKED_HAZARD_RESULTS_OUTPUT.format(damOrLeveeName, currentHazardType, currentEventID, status.lower())
                        newEventSet.addAttribute(RESULTS_OUTPUT_KEY, resultsText)
                        return newEventSet

        if hazardEvent is None:
            hazardEvent = EventFactory.createEvent()

        hazardEvent = self.updateEventAttributes(hazardEvent, eventSet.getAttributes(), \
                                      dialogInputMap, spatialInputMap, damOrLeveeName)

        if not hazardEvent:
            resultsText += self.MISSING_HAZARD_RESULTS_OUTPUT.format(damOrLeveeName)
            newEventSet.getEvents().clear()
        elif not hazardEvent.getGeometry() or not hazardEvent.getGeometry().is_valid:
            resultsText += self.HAZARD_INVALID_GEOMETRY_RESULTS_OUTPUT.format(damOrLeveeName)
            newEventSet.getEvents().clear()
        else:
            newEventSet.add(hazardEvent)

        newEventSet.addAttribute(RESULTS_OUTPUT_KEY, resultsText)
        return newEventSet

    def toString(self):
        return "OldRiverSpecial"

    def updateEventAttributes(self, hazardEvent, sessionDict, dialogDict, spatialDict, damOrLeveeName):
        """
        Creates the hazard event, based on user dialog input and session dict information.
       
        @param hazardEvent: An empty hazard event to fill with hazard information.
                            This is injectable so that test versions of this object
                            can be used.
        @param sessionDict: A dict of Hazard Services session information
        @param dialogDict: A dict of Hazard Services dialog information  
        @param spatialDict: A dict of Hazard Services spatial input information
       
        @return: A hazard event representing a dam break flash flood watch or warning
        """
        # If relying on existing impact area for this dam, use the self.origKey_forDam
        # argument. If a new impact area has been installed specifically for this critical
        # scenario, use damOrLeveeName.
        hazardGeometry = self.getFloodPolygonForDam(self.origKey_forDam)

        riverName = self.getRiverNameForDam(damOrLeveeName)

        significance = "A"
        subType = None

        urgencyLevel = dialogDict["urgencyLevel"]
       
        if "WARNING" in urgencyLevel:
            significance = "W"
            subType = "NonConvective"
           
        currentTime = long(sessionDict["currentTime"])
        startTime = currentTime
        endTime = startTime + self.DEFAULT_FFW_DURATION_IN_MS
       
        if hazardEvent.getEventID() == None:
            hazardEvent.setEventID("")
            hazardEvent.setHazardStatus("PENDING")
           
        hazardEvent.setSiteID(str(sessionDict["siteID"]))
        hazardEvent.setPhenomenon("FF")
        hazardEvent.setSignificance(significance)
        hazardEvent.setSubType(subType)

        # New recommender framework requires some datetime objects, which must
        # be in units of seconds.
        hazardEvent.setCreationTime(datetime.datetime.utcfromtimestamp(
                                   currentTime / GeneralConstants.MILLIS_PER_SECOND))
        hazardEvent.setStartTime(datetime.datetime.utcfromtimestamp(
                                   startTime / GeneralConstants.MILLIS_PER_SECOND))
        hazardEvent.setEndTime(datetime.datetime.utcfromtimestamp(
                                   endTime / GeneralConstants.MILLIS_PER_SECOND))
        hazardEvent.setGeometry(GeometryFactory.createCollection([hazardGeometry])
                                   if hazardGeometry else None)
       
        cwaAndMarineZoneGeometry = sessionDict["cwaAndMarineZoneGeometry"]
        preparedGeometry = prep(cwaAndMarineZoneGeometry)
        withinCWAandMarineZone = preparedGeometry.covers(hazardEvent.getGeometry())
       
        hazardEvent.setHazardAttributes({"cause":"Dam Failure",
                                          "damOrLeveeName":damOrLeveeName,
                                          HazardConstants.RECOMMENDED_EVENT: True,
                                          "riverName": riverName,
                                          "preDefinedArea": True,
                                          "preDefinedAreaEditable": withinCWAandMarineZone
                                         })
        return hazardEvent

    def getFloodPolygonForDam(self, damOrLeveeName):
        """
        Returns a user-defined flood hazard polygon for
        a dam. The base version of this tool does nothing. It is up to the implementer
        to override this method.
       
        @param  damOrLeveeName: The name of the dam or levee for which to retrieve a
                         hazard polygon
                           
        @return Geometry: A Shapely geometry representing
                          the flood hazard polygon
        """
        if damOrLeveeName in self.damPolygonDict:
            return GeometryFactory.createPolygon(self.damPolygonDict[damOrLeveeName])
        else:
            return None

    def getRiverNameForDam(self, damOrLeveeName):
        """
        Returns the River that the Dam or Levee is located on.
        """
        damMetaData = self.mapsAccessor.getDamInundationMetadata(damOrLeveeName)
        if damMetaData:
            return damMetaData.get("riverName", None)
        return None

    def createResultsStringFromEventSet(self, eventSet):
        resultsString = ''
        for event in eventSet:
            status = event.getStatus()
            hazardType = self.HAZARD_TYPE_MAP.get(event.getSignificance())
            eventID = event.getDisplayEventID()
            if event.isProposed():
                resultsString += "{} {} Event: {} has been updated.\n".format(status.capitalize(), hazardType, eventID)
            elif status == "PENDING":
                resultsString += "{} Event: {} has been created or updated.\n".format(hazardType, eventID)
            elif status == "ENDING":
                resultsString += "{} Event: {} has been set to {}.\n".format(hazardType, eventID, status.capitalize())
            else:
                self.logger.exception("Dam Levee Flood Tool produced invalid event.")
        return resultsString

    # removed.
    # def createLabelDropDownDict(self, damOrLeveeNameList):

The next thing is to register this new recommender and give it a suitable title on the Tools menu.  This is done with an override of CommonSettings.py. Here we leverage the default incremental override behavior of lists to keep this override very simple.

CommonSettings = {
    "toolbarDefinitions": {
        "OldRiverSpecial" : {
            "displayName": "Old River Dam Emergency",
            "toolType": "USER_RECOMMENDER",
        },
    },
}

Another thing needed is to supply a DamMetaData entry for this new dam key, "Old River Dam Special". First we show the existing entry for this dam so that one can understand what is being done different for the new entry:
"Old River Dam": {

            "riverName": "Old River",

            "damName": "Old River Dam",

            "cityInfo": "Manawa, located just below",

            "dropDownLabel": "Old River Dam",

            "featureID": "OldRiverDam"

        },

What follows is the override for DamMetaData.py.  Note that the value for "damName", which is put into product text, is still the same.  Also note that the the "ibwType"  is set to 'catastrophic', which will cause it to start out as being an EMERGENCY hazard, and that a response scenario has been added. That scenario was made up to support this example; any scenario for operations would of course need to be valid for the situation, and it is not mandatory to add one.

DamMetaData = {
    "Old River Dam Special": {
        "riverName": "Old River",
        "damName": "Old River Dam",
        "cityInfo": "Manawa, located just below",
        "dropDownLabel": "Old River Dam",
        "featureID": "OldRiverDam",
        "ibwType" : 'catastrophic',
        "hydrologicCause": "siteFailed",
        "scenarios": {
            "highfast": {
                "displayString": "high fast",
                "productString": "If a complete failure of the dam occurs, the water depth at Iola could exceed 25 feet in 4 minutes."
            },
        },
    },
}

Finally, this recommender needs to be added to any settings that it is important for. While straightfoward, this can be the most challenging aspect of this because it can be tedious. Over time, many users will create their own user instance of some of the default settings, or perhaps their own customized settings, by way of the Edit Setting: dialog.  The way these user settings files are written out, the list of recommenders to have under the Tools menu for the setting is specified in a completely non-incremental way.  That means that once such a user instance of a setting is created, it becomes impossible to affect the list of applicable recommenders for that setting by way of a Region, Configured, or Site override of that settings file. So in the case where a new customized setting is introduced, the only practical way to deal with making it available is to have each user go into the Recommenders tab of the Edit Setting: dialog, and manually add the new recommender to each setting where it is important, and then save the updated instance of those settings.  While doing this, a particularly critical recommender can be moved to the very top of the list of applicable recommenders, so that it is right at the user’s fingertips.

3.2 Hazard Metadata     contents    ^  v   

At the beginning of hazard metadata modules, generally a variable self.hazardEvent is set for the class; this contains a local working copy of the hazard event the metadata is being generated on behalf of. It is very common for metadata logic to inspect the contents of this data structure during the creation of the metadata. However, metadata logic should never change the contents of self.hazardEvent.


Another caution; if one is using the “refreshMetadata” feature to modify an existing megawidget that retains the same "fieldName" attribute, great care must be taken if one is also changing the "fieldType". In general this is not recommended.  More about this in section 3.2.7.

When errors involving executing metadata show up in alertviz or console logs, the way they are reported is a string representation of the entire returned metadata (which can be a huge amount of text), followed by a description of the error encountered.  So one needs to pay close attention when scanning such an error message for the error description. Note that if a method in a metadata class constructs a proper megawidget element but neglects to actually return it, this will not cause a python compile error.  Instead the metadata will execute and an error message as just described will be output;  the error description in this case might only make a vague reference to some of the content being “null”.

3.2.1 Examples of changes in Calls to Action     contents    ^  ─  +  v   

In this section we will present some customization examples for calls to action. When making modifications to calls to action, it is very important to be mindful of the distinction between the getCTA_Choices() method and the getCTAs() method. getCTA_Choices() is where the list of all calls to action that apply to a hazard is specified. getCTAs() packages those choices as a checkbox menu and controls which are activated by default.  Also be aware that the methods that create the megawidget element for each specific CTA are neither in CommonMetaData.py nor in any of the individual metadata modules; they are all in the module CallsToActionAndImpacts.py.

3.2.1.1 Change the wording of a Call to Action  contents    ^  ─  +  v   

This example is to change the wording of the “Nightime flooding” call to action. First, one needs to find which python module this call to action is defined in. These commands will accomplish this (see Introduction for more information about searching code for functionality):

> cd /awips2/edex/data/utility/common_static/base/HazardServices

> find . -name '*.py' -exec grep -i 'Nighttime flooding' '{}' \; -print

In this case the file identified will be CallsToActionAndImpacts.py; the base version of all call to action methods are defined here. Search the module CallsToActionAndImpacts.py for the string “Nightime flooding”, and you will see that the method involved is ctaNightTime(). To implement the wording change for all hazard types, create an override file for CallsToActionAndImpacts.py with the following (change from base in green):

class CallsToActionAndImpacts(object):

    def ctaNightTime(self):
        return  {"identifier": "nighttimeCTA",
                "displayString": "Nighttime flooding",
                "productString":
                '''Be especially cautious at night when it is harder to recognize the
                dangers of flooding. Do not stay in areas subject to flooding when
                water begins rising.
'''}

Now suppose there was a desire to change the wording of this call to action for only one hazard type; for sake of argument let’s say convective FFWs. Here is what this override would look like in this case:

class CallsToActionAndImpacts(object):

    def ctaNightTime(self):
        prodString = '''Be especially cautious at night when it is harder to recognize the
                        dangers of flooding.'''
        import traceback
        tbData = traceback.format_stack()
        i = len(tbData)-1
        while i>=0 :
            onetb = tbData[i]
            i -= 1
            if onetb.find("MetaData")<0 :
                continue
            if onetb.find("FF_W_Convective")>=0 :
                prodString +=  ''' Do not stay in areas subject to flooding when
                                   water begins rising.'''
            break

        return  {"identifier": "nighttimeCTA",
                "displayString": "Nighttime flooding",
                "productString": prodString }

What is happening here is that the traceback utility is used to determine whether this method is being invoked by the MetaData_FF_W_Convective.py metadata class, and updating the productString accordingly if this is the case.

3.2.1.2 Change list of Calls to Action for a Hazard     contents    ^    +  v   

This customization example will be to change the the default call to action choices for a particular hazard type. Specifically, we will add the "Turn around...don't drown" call to action to the list available for the Areal Flood Watch. Use commands similar to those in the previous example to learn that the default version of this call to action method is implemented CallsToActionAndImpacts.py; inspect CallsToActionAndImpacts.py to learn that the method name is ctaTurnAround(). The specific metadata method for Areal Flood Watch is MetaData_FA_A.py. To make this change, you need to provide an override for the getCTA_Choices() method of MetaData_FA_A.py as follows (change from base in green):

class MetaData(CommonMetaData.MetaData):

    def getCTA_Choices(self):
        return [
            self.ctaImpact.ctaSafety(),
            self.ctaImpact.ctaStayAway(),
            self.ctaImpact.ctaTurnAround(),
            self.ctaImpact.ctaFloodWatchMeans()
            ]

3.2.1.3 Change Calls To Action activated by default     contents    ^    +  v   

This customization example will be to make the "Stay away" call to action be activated by default for an Areal Flood Watch. First, one needs to locate the call to action method in CallsToActionAndImpacts.py (ctaStayAway) and note the value of the identifier specified in that method, which is "stayAwayCTA". Then we need to create an override for the getCTAs() method of MetaData_FA_A.py. Note that the default instance of the getCTAs() method for MetaData_FA_A.py is actually in CommonMetaData.py. The content of this override follows (change to base instance in green):

class MetaData(CommonMetaData.MetaData):

    def getCTAs(self, values=None):
        pageFields = {
             "fieldType":"CheckBoxes",
             "fieldName": "cta",
             "showAllNoneButtons" : False,
             "choices": self.getCTA_Choices()
         }

        if values is not None:
            pageFields["values"] = values
        else :
            pageFields['values'] = [ "stayAwayCTA" ]


        return {
                
               "fieldType": "ExpandBar",
               "fieldName": "CTABar",
               "expandHorizontally": True,
               "pages": [
                            {
                             "pageName": "Calls to Action",
                             "pageFields": [pageFields]
                            }
                         ],
                "expandedPages": ["Calls to Action"]
                }

Of course, if there were already a site version of MetaData_FA_A.py, the instance of getCTAs() shown would be added to that. The only code added to this is the code in green, which makes the CTA with the identifier of "stayAwayCTA" the one that is turned on by default for a new hazard. The two lines of code immediately preceding that allows an existing set of activated CTAs selected by the forecaster to be propagated to the next product in the life cycle.

3.2.1.4  Remove an existing default Call To Action  contents    ^    +  v   

In this example we remove any calls to action referring to arroyos from those available for convective Flash Flood warnings. For this example this will be approached differently from other examples where the set of available calls to action were changed; we will show how to implement this by overriding getCTAs(). Note that the base version we are overriding here is originally from CommonMetaData.py.

What follows is new site version of MetaData_FF_W_Convective.py for this purpose. The code added or changed is in green as before. The point here is not to necessarily recommend this as a superior approach to implementing this in getCTA_Choices(); rather to show that this is possible and to point out an advantage of this approach. The main advantage is that any future update of the getCTA_Choices() method in the base MetaData_FF_W_Convective.py will be far less likely to require a manual merge to both pull in any new CTAs but still filter out arroyo related CTAs. Note again that the default instance of getCTAs() for this hazard type is actually in CommonMetaData.py.

class MetaData(CommonMetaData.MetaData):

    def getCTAs(self, values=None):

        defChoices =  self.getCTA_Choices()
        myChoices = [ ]
        for defChoice in defChoices :
            if defChoice.has_key("identifier") and \
               defChoice["identifier"].lower().find("arroyos")<0 :
                myChoices.append(defChoice)

        pageFields = {
             "fieldType":"CheckBoxes",
             "fieldName": "cta",
             "showAllNoneButtons" : False,
             "choices": myChoices
         }

        if values is not None:
            pageFields["values"] = values

        return {
                 
               "fieldType": "ExpandBar",
               "fieldName": "CTABar",
               "expandHorizontally": True,
               "pages": [
                            {
                             "pageName": "Calls to Action",
                             "pageFields": [pageFields]
                            }
                         ],
                "expandedPages": ["Calls to Action"]
                }

3.2.1.5 Add a new Call To Action  contents    ^    +  v   

In this final customization example related to calls to action, we show how to add a new call to action. First we need to add a new method for the content of the call to action. These methods always go into an override of CallsToActionAndImpacts.py:

class CallsToActionAndImpacts(object):

    def ctaDrivingInstruction(self):
        return {"identifier": "drivingCTA",
                "displayString": "Driving instructions",
                "productString":
                '''Do not drive your vehicle into areas where the water covers the
                roadway. The water depth may be too great to allow your car to
                cross safely. Move to higher ground. Motorists should not attempt
                to drive around barricades or drive cars through flooded areas.''' }

To complete this, the new call to action content method needs to be added to the getCTA_Choices() method in the metadata file for the desired hazard type(s). Here is what such an override would look like added in MetaData_FA_A.py:

class MetaData(CommonMetaData.MetaData):

    def getCTA_Choices(self):
        return [
            self.ctaSafety(),
            self.ctaStayAway(),
            self.ctaDrivingInstruction(),
            self.ctaFloodWatchMeans()
            ]

3.2.2 Changing default Hydrologic Cause     contents    ^    +  v   

The purpose of this customization example is to change the default hydrologic cause for non-convective flash flood warnings to be “Ice Jam” in winter and “Snow Melt” otherwise. The file being overridden here is MetaData_FF_W_NonConvective.py (changes to base in green).

class MetaData(CommonMetaData.MetaData):

    def execute(self, hazardEvent=None, metaDict=None):
        self.initialize(hazardEvent, metaDict)

        # Adapt value of self.HYDROLOGIC_CAUSE_DEFAULT to day of year
        creationTime = self.hazardEvent.getCreationTime()
        date0 = creationTime.date()
        month = date0.month
        day = date0.day
        if month in [1, 2] or (month == 12 and day >= 22) or (month == 3 and day < 22):
            self.HYDROLOGIC_CAUSE_DEFAULT = 'icejam'
        else:
            self.HYDROLOGIC_CAUSE_DEFAULT = 'snowMelt'

        self.leveeCause = self.hydrologicCauseLevee()["identifier"]
        self.floodgateCause = self.hydrologicCauseFloodGate()["identifier"]
        self.icejamCause = self.hydrologicCauseIceJam()["identifier"]
        self.ijbreakCause = self.hydrologicCauseIceJamBreak()["identifier"]
        self.glacierCause = self.hydrologicCauseGlacialOutburst()["identifier"]

        self.addDamList = [
              self.hydrologicCauseDam()["identifier"],
              self.hydrologicCauseSiteImminent()["identifier"],
              self.hydrologicCauseSiteFailed()["identifier"],
              self.leveeCause,
              self.floodgateCause
              ]

        self.addVolcanoList = [
                  self.hydrologicCauseVolcano()["identifier"],
                  self.hydrologicCauseVolcanoLahar()["identifier"]
                  ]

        self.featurelessCauses = [
                  self.hydrologicCauseRain()["identifier"],
                  self.hydrologicCauseSnowMelt()["identifier"]
                  ]

        self.hydrologicCause = None
        self.damOrLeveeName = None
        self.hazardLocation = ""
        self.damMetadata = None
        self.riverName = None
        self.recommendedEvent = False
        if hazardEvent is not None:
            self.recommendedEvent = hazardEvent.get('recommendedEvent', False)
            # The dictionary default mirrors the default in getHydrologicCause()
            self.hydrologicCause = hazardEvent.get("hydrologicCause", self.HYDROLOGIC_CAUSE_DEFAULT)
            self.damOrLeveeName = hazardEvent.get('damOrLeveeName')
            if isinstance(self.damOrLeveeName, str) :
                if self.damOrLeveeName.find("|")<0 and self.damOrLeveeName.find("*")<0 :
                    self.hazardLocation = self.damOrLeveeName
                if self.hazardLocation==self.damOrLeveeName or \
                   self.hydrologicCause in self.addDamList :
                    from MapsDatabaseAccessor import MapsDatabaseAccessor
                    mapsAccessor = MapsDatabaseAccessor()
                    self.damMetadata = \
                      mapsAccessor.getDamInundationMetadata(self.damOrLeveeName, True)
            if isinstance(self.damMetadata, dict) :
                self.riverName = self.damMetadata.get("riverName")
                metaHydroCause = self.damMetadata.get("hydrologicCause")
                if metaHydroCause :
                    self.hydrologicCause = metaHydroCause
                altLoc = self.damMetadata.get("breachLocation")
                if altLoc :
                    self.hazardLocation = altLoc
                altLoc = self.damMetadata.get("iceJamLocation")
                if altLoc :
                    self.hazardLocation = altLoc
                altLoc = self.damMetadata.get("glacierLocation")
                if altLoc :
                    self.hazardLocation = altLoc
                altLoc = self.damMetadata.get("volcanoName")
                if altLoc :
                    self.hazardLocation = altLoc

        metaData = self.buildMetaDataList()

        metaData += self.getDoesNotContributeToLockingBolding(False)
        return {
                HazardConstants.METADATA_KEY: metaData
                }

3.2.3 Adapt the metadata to the VTEC life cycle stage.   contents    ^  v   

At times there might be a desire to have the content of the metadata adapt to the stage in the VTEC life cycle (e.g. NEW, CON, etc.) the product being issued will have. This already happens to some degree, mostly where a distinction is made between hazards that are ending and hazards that are not ending.  Recent changes make it straightforward to have the metadata adapt more precisely to the specific stage in the VTEC life cycle that applies to the hazard being generated. These changes mean that nearly all the ambiguity with respect to the life cycle has been eliminated.  This can now be determined by checking the content of the following metadata member variables:
       self.hazardStatus  self.isCANset  self.isEXPset

The table that follows shows the state of those variables for each VTEC life cycle stage:

Impending Product
VTEC Stage
self.hazardStatusself.isCANsetself.isEXPset
NEW"pending"FalseFalse
CON"issued"FalseFalse
EXT"issued"FalseFalse
CAN"ending"TrueFalse
EXP"ending"FalseTrue

Note that there is one instance of ambiguity; one cannot distinguish between CON and EXT.  The main reason this ambiguity still exists is that when transitioning from issuing a CON to issuing an EXT (by changing Add to End Time), the metadata will never be reexecuted and so there will never be an opportunity to actually change the metadata in response to this.

Another aspect of the impending product being issued that could possibly impact the design of the metadata is whether or not it is a correction. This is also now completely unambiguous; a metadata member variable exists, self.isCORClicked, which will be True if a correction is being issued. Everything in the previous table will apply to the product that is about to be issued, regardless of whether or not it is a correction.

Here is a very simple override that leverages this. The question of whether the immediate cause should be modifiable for a correction is something that has spawned endless debate, and we are not necessarily recommending this. However, for the sake of argument, suppose one did want to lock out changes to the immediate cause for a correction.  Here is an override that can implement this:

CommonMetaData.py:

class MetaData(object):

    def getImmediateCause(self, values='ER'):
       choices = self.immediateCauseChoices()
       return {
            "fieldName": "immediateCause",
            "fieldType":"ComboBox",
            "label":"Immediate Cause:",
            "values": values,
            "expandHorizontally": False,
            "choices": choices,
            "enable" : not self.isCORClicked,
            "refreshMetadata": True,
            }

There are some downsides to the ambiguity between CON and EXT.  Because an EXT is an update that does not end the hazard, the information needed will most of the time be very similar to what is needed for a CON.  However, it is true that for some hazard types, the product format used for an EXT is much closer to what is used for a NEW than for a CON. This is because an EXT brings new future times under threat, which can necesitate things like EAS activation.  So some aspects of the information needed for an EXT may be more like that needed for a NEW.

Therefore, let us suppose that a situation was encountered where it was important to have the metadata for an EXT be different than that for a CON.  There is a solution available, albeit a clunky one.  First, create the override that follows for CommonMetaData.py, which introduces a new boolean, self.isEXTset:

CommonMetaData.py:

import HazardDataAccess

class MetaData(object):

    def initialize(self, hazardEvent, metaDict):
        self.hazardEvent = hazardEvent
        self.metaDict = metaDict

        # Set current time
        simutime = int((SimulatedTime.getSystemTime().getMillis()/1000 + SECONDS_PER_HOUR)/SECONDS_PER_HOUR) * SECONDS_PER_HOUR
        self.currentTime = int(time.mktime(datetime.datetime.utcfromtimestamp(simutime).timetuple()))

        if self.hazardEvent:
            self.hazardStatus = self.hazardEvent.getStatus().lower()
        else:
            self.hazardStatus = "pending"

        # Set flags for "End This" being clicked, "Expire This" being clicked,
        # or the Hazard Event otherwise ending.
        self.isEXPset = False
        self.isCANset = False
        self.isCORClicked = self.hazardEvent.get("correctThisClicked",False)
 
        if self.hazardStatus.lower() == "ending":
            if self.hazardEvent.get("forceVTEC") == "EXP":
                self.isEXPset = True
            else:
                self.isCANset = True

        self.isEXTset = not (self.isEXPset or self.isCANset or \
                        self.isCORClicked or self.hazardStatus=="pending" )
        self.lastEvent = self.hazardEvent
 
        if self.isCORClicked or self.isEXTset:
            isPractice = self.hazardEvent.get("practice")
            eventId = self.hazardEvent.getEventID()
            historyList = HazardDataAccess.getHazardHistoryEvents(eventId, isPractice)
            vtecs = self.getVTECsBeforeCorrections(historyList)

            if self.isEXTset :
                if self.hazardEvent.getEndTime()==self.lastEvent.getEndTime() :
                    self.isEXTset = False
            elif "CON" in vtecs:
                pass
            elif "CAN" in vtecs:
                self.isCANset = True
                self.hazardStatus = "ending"
            elif "EXP" in vtecs:
                self.isEXPset = True
                self.hazardStatus = "ending"
            elif "EXT" in vtecs:
                self.isEXTset = True
            elif "NEW" in vtecs:
                self.hazardStatus = "pending"

        self.riverForecastUtils = None
        self.riverForecastPoint = None

    def getVTECsBeforeCorrections(self, historyList):
        vtecCodes = None

        for prevEvent in historyList:
            vtecCodes = prevEvent.get("vtecCodes")
            if not 'COR' in vtecCodes:
                self.lastEvent = prevEvent
                return vtecCodes

        return vtecCodes


Then there needs to be a way to get the metadata to reexecute after changing Add to End Time.  Here is where the clunkiness comes in.  The override that follows demonstrates this for MetaData_FA_Y.py; it adds a button to the top of the Details section labelled “For EXT’. The idea is that after making a choice in Add to End Time, the user hits that new button and the metadata reexecutes.  Certainly not an ideal long term solution.

MetaData_FA_Y.py:

                     
class MetaData(CommonMetaData.MetaData):

    def execute(self, hazardEvent=None, metaDict=None):
        self.initialize(hazardEvent, metaDict)
        if self.hazardStatus in ["ending", "ended", "elapsing", "elapsed"]:
            metaData = [
                        self.getEndingOption(),
                        ]
        else: # issued/pending
            source = self.getSource()
            eventType = self.getEventType()
            self.ctaImpact = CallsToActionAndImpacts()
            extButton = { "fieldType" : "Button", "fieldName": "EXT",
                          "label" : "For EXT", "refreshMetadata": True }
            metaData = [
                    extButton,
                    self.getImmediateCause(),
                    source,
                    eventType,
                    self.getAdvisoryType(),
                    self.getOptionalSpecificType(),
                    self.getMinorFloodOccurring(),
                    self.getRainAmt(),
                    self.getLocationsAffected(),
                    self.lidImpactsList(),
                    self.getAdditionalInfo(notation="comments"),
                    self.getCTAs(self.hazardEvent.get("cta")),
                ]
            if hazardEvent:
                immediateCause = hazardEvent.get('immediateCause')
                # Immediate causes that should use a default value of 'None'
                # for event type and 'Public reported' for source
                icsWithDifferentDefaults = [self.immediateCauseIJ()['identifier'],
                                      self.immediateCauseGO()['identifier']]
                if immediateCause == self.immediateCauseDR()['identifier']:
                    damOrLeveeName = hazardEvent.get('damOrLeveeName')
                    metaData.insert(2, self.getDamOrLevee(damOrLeveeName))
                elif immediateCause in icsWithDifferentDefaults:
                    source['values'] = self.publicSource()['identifier']
                    eventType['values'] = self.noEventType()['identifier']

                # Force event types to default value when switching between
                # immediate causes with different default values
                prevMetadata = hazardEvent.get('prevMetadata', {})
                prevImmediateCause = prevMetadata.get('immediateCause')
                icHasDifferentDefaults = immediateCause in icsWithDifferentDefaults
                prevICHasDifferentDefaults = prevImmediateCause in icsWithDifferentDefaults
                if (icHasDifferentDefaults != prevICHasDifferentDefaults):
                    source['useNewValueOnRefresh'] = True
                    eventType['useNewValueOnRefresh'] = True
                prevMetadata['immediateCause'] = immediateCause
                hazardEvent.addHazardAttribute('prevMetadata', prevMetadata)

        return {
                HazardConstants.METADATA_KEY: metaData
                }


3.2.4 Modifying the Product Staging Dialog     contents    ^  v   

The Product Staging Dialog is a dialog that sometimes comes up after choosing Preview but before the Product Editor appears. The purpose of this dialog is to allow the user to make choices that are for the product as a whole rather than for one particular hazard; this typically  occurs for hazard types where a single product can possibly contain text for multiple specific hazards.

Files that define metadata for a hazard type have the hazard
phensig for the hazard type encoded into the metadata file name (e.g. MetaData_FA_W.py). Files that define metadata for a Product Staging Dialog have the NNN of the product(s) to generate encoded into the metadata file name (e.g. MetaData_FFA_FLW_FLS.py).


Here we have a customization example where all the synopsis options that refer to snow melt are commented out; one might do this if one was at a WFO in a place like Southern Florida. We also make it so the category increase synopsis results in a first guess for the language describing the rivers involved. This is done by overriding the methods execute(), synopsisCategoryIncrease(), and getSynopsisChoices() in MetaData_FFA_FLW_FLS.py (changes from base in
green).

class MetaData(CommonMetaData.MetaData):

    def execute(self, hazardEvents=None, metaDict=None):
        self.hazardEvents = hazardEvents
        productSegmentGroup = metaDict.get('productSegmentGroup')
        self.productCategory = metaDict.get('productCategory')
        self.stagingDialogValues = metaDict.get("stagingDialogValues")

        self.productLabel = productSegmentGroup.get('productLabel')
        self.prevProductLabel = self.hazardEvents[0].get('productLabel', None)
        suffix = "_" + self.productLabel
        prevSuffix = None
        if self.prevProductLabel:
            prevSuffix = "_" + self.prevProductLabel

        geoType = productSegmentGroup.get('geoType')
        productID = productSegmentGroup.get('productID')
        self.eventIDs = productSegmentGroup.get('eventIDs')

        self.streamNames = set()
        self.immediateCauses = set()
        self.allCAN = True
        if self.hazardEvents:
            for hazardEvent in self.hazardEvents:
                self.streamNames.add(hazardEvent.get('streamName', ''))
                self.immediateCauses.add(hazardEvent.get('immediateCause', 'ER'))
                if hazardEvent.getStatus() not in ['ENDING', 'ENDED']:
                    self.allCAN = False
                    break

        # Product level CTA's are only for the point-based hazards
        ctas = {}
        if geoType == 'point':
            if not self.allCAN:
                previousValues = set()
                for hazard in self.hazardEvents:
                    tempVals = self.getPreviousStagingValue(hazard, "callsToAction_productLevel")
                    if tempVals:
                        previousValues.update(tempVals)
                ctas = self.getCTAs(previousValues)
        else:
            # There is no overviewSynopsis product part if the segment has only CAN, EXP in it
            if hazardEvent.getStatus() in ['ELAPSED', 'ENDING', 'ENDED']:
                return {
                    HazardConstants.METADATA_KEY:[]
                }

        metaData = []

        # Only add the overview if there is canned choices
        choices = self.getSynopsisChoices()
        if choices:
            # Determine if there is a previous value to use
            previousValues = set()
            for hazard in self.hazardEvents:
                previousValue = self.getPreviousStagingValue(hazard, "overviewSynopsisCanned")
                if previousValue:
                    previousValues.add(previousValue)

            prevValue = None
            if previousValues:
                prevValue = list(previousValues)[0]
            overview = {
                    "fieldType": "ComboBox",
                    "fieldName": "overviewSynopsisCanned" + suffix,
                    "label": "Overview Synopsis for " + self.productLabel,
                    "choices": choices,
                    "values" : prevValue,
                    }
            metaData.append(overview)

        if ctas:
            metaData.append(ctas)
        return {
            HazardConstants.METADATA_KEY: metaData
            }

    def synopsisCategoryIncrease(self):
        riverDescription = None
        for streamName in self.streamNames :
            if streamName=='' :
                continue
            if riverDescription==None :
                riverDescription = streamName+'.'
            elif riverDescription.find(' and ')>0 :
                riverDescription = streamName+', '+riverDescription
            else :
                riverDescription = streamName+' and '+riverDescription
        if riverDescription==None :
            riverDescription = "|* riverName *|."
        if self.productLabel.find('FFA')>=0:
            productString = "Heavy rainfall may increase the severity of flooding on the "
        else:
            productString = "Heavy rainfall will increase the severity of flooding on the "
        productString += riverDescription
        return {"identifier":"categoryIncrease",
                "displayString":"Increase in Category",
                "productString": productString,
        }

    def getSynopsisChoices(self):
        # No options make sense for CANs
        if self.allCAN:
            return []
        choices = [
                    self.synopsisBlank(),
                    #self.synopsisTempSnowMelt(),
                    #self.synopsisDayNightTemps(),
                    #self.synopsisSnowMeltReservoir(),
                    #self.synopsisRainOnSnow(),
                    #self.synopsisIceJam(),
                    self.synopsisCategoryIncrease()
                ]

        identifiers = set()
        for immediateCause in self.immediateCauses:
            if immediateCause == 'SM':
                #identifiers.add(self.synopsisTempSnowMelt().get('identifier'))
                #identifiers.add(self.synopsisDayNightTemps().get('identifier'))
                #identifiers.add(self.synopsisSnowMeltReservoir().get('identifier'))
                identifiers.add(self.synopsisCategoryIncrease().get('identifier'))
            #elif immediateCause == 'RS':
                #identifiers.add(self.synopsisRainOnSnow().get('identifier'))
            elif immediateCause == 'IC':
                #identifiers.add(self.synopsisTempSnowMelt().get('identifier'))
                #identifiers.add(self.synopsisDayNightTemps().get('identifier'))
                #identifiers.add(self.synopsisSnowMeltReservoir().get('identifier'))
                #identifiers.add(self.synopsisRainOnSnow().get('identifier'))
                #identifiers.add(self.synopsisIceJam().get('identifier'))
                identifiers.add(self.synopsisCategoryIncrease().get('identifier'))
            #elif immediateCause == 'IJ':
                #identifiers.add(self.synopsisIceJam().get('identifier'))
            elif immediateCause == 'MC':
                #identifiers.add(self.synopsisTempSnowMelt().get('identifier'))
                #identifiers.add(self.synopsisDayNightTemps().get('identifier'))
                #identifiers.add(self.synopsisSnowMeltReservoir().get('identifier'))
                #identifiers.add(self.synopsisRainOnSnow().get('identifier'))
                #identifiers.add(self.synopsisIceJam().get('identifier'))
                identifiers.add(self.synopsisCategoryIncrease().get('identifier'))
            elif immediateCause == 'UU':
                #identifiers.add(self.synopsisTempSnowMelt().get('identifier'))
                #identifiers.add(self.synopsisDayNightTemps().get('identifier'))
                #identifiers.add(self.synopsisSnowMeltReservoir().get('identifier'))
                #identifiers.add(self.synopsisRainOnSnow().get('identifier'))
                #identifiers.add(self.synopsisIceJam().get('identifier'))
                identifiers.add(self.synopsisCategoryIncrease().get('identifier'))
            elif immediateCause == 'DR':
                #identifiers.add(self.synopsisSnowMeltReservoir().get('identifier'))

        availableChoices = []
        for identifier in identifiers:
            for choice in choices:
                if choice.get('identifier') == identifier:
                    availableChoices.append(choice)
                    break

        if availableChoices:
            # Give a option for no canned text.
            availableChoices.insert(0, self.synopsisBlank())
        return availableChoices

3.2.5 Change default search parameters for impacts section of flood warning products     contents    ^  v   
         
Here we describe how to change the default search parameters for the impacts sections of point flood warning products. When the CrestsAndImpactsUtil.py module was introduced, several small methods were added that specifically carry default values for several of these selectors. For the sake of argument, suppose we want to change the default value of 'Maximum Depth Below Flood Stage' from -3 to -5.  The following override to CrestsAndImpactsUtil.py will accomplish this:
class CrestsAndImpactsUtil(object):

    def getDepthBelowFloodStageDefaultValue(self, parm):
        #return -3
        return -5

This override assumes that the exact same defaults are desired for FL.W, FL.A, and FL.Y hazards. If it was desired to have different defaults among these hazards, this would require an override to either MetaData_FL_W.py, MetaData_FL_A.py, or MetaData_FL_Y.py. Here we show one way to apply this same change to only FL.W hazards.

First generate any point flood warning recommendation. Click on a recommendation in the Hazard Services console to pop the Hazard Information Dialog. Choose the Impacts Statement tab and then click on Impacts Search Parameters to expand that dialog. As mentioned, for the purpose of this exercise we are going to change the default for 'Maximum Depth Below Flood Stage'. If there was a different element one wanted to make such a hazard type specific change for, viewing the dialog would allow one to find an appropriate string to search for.So now use the following commands to learn where this dialog is being constructed (see Introduction for more information about searching code for functionality):

> cd /scratch/albatross/awips2/edex/data/utility/common_static/base
> touch /tmp/bbb
> find HazardServices -name '*py' -exec grep 'Maximum Depth Below' '{}' /tmp/bbb \;

This tells us that HazardServices/hazardMetaData/CommonMetaData.py is where this dialog is constructed. Inspecting CommonMetaData.py, one learns that the method which constructs this dialog is getStageWindow(). So to change the default search parameters one needs to create a site override of this method. This could be in a site version of CommonMetaData.py, which would impact all hazard types using this dialog, or it could be in MetaData_FL_W.py, which would impact only this particular hazard type. For this exercise, we will implement this in MetaData_FL_W.py. What follows is the user override of MetaData_FL_W.py that implements this (changes from base in
green; note that very little is a copy of the base).

import traceback

class MetaData(CommonMetaData.MetaData):

    # General method for recursively traversing a megawidget structure and
    # updating the default value for the field name containing a substring
    def updateDefaultValues(self, inmeta, fieldNameSub, newvalue, depth=0) :
        if not isinstance(inmeta, dict) :
            return inmeta
        if "values" in inmeta :
            fn = inmeta.get("fieldName")
            if not isinstance(fn, str) :
                return inmeta
            if fn.find(fieldNameSub)>=0 :
                inmeta["values"] = newvalue
            return inmeta
        fldList = inmeta.get("fields")
        if not isinstance(fldList, list) :
            return inmeta
        n = len(fldList)
        i = 0
        depth += 1
        while i<n :
            fldList[i] = self.updateDefaultValues(\
                              fldList[i], fieldNameSub, newvalue, depth)
            i += 1
        return inmeta

    def getStageWindow(self,parm,low=-4,hi=4):

       # Call the base class method to get initial default version of
        # metadata; if not for the impacts we just pass that back.
        basemeta = super(MetaData ,self).getStageWindow(parm, low, hi)
        if parm != 'impacts' :
            return basemeta

        # Set traceNow = True to diagnose the behavior of this routine
        traceNow = True
        if traceNow :
            hhhhh = open("/tmp/getStageWindow.txt", "w")
            hhhhh.write("from MetaData_FL_W.py\n")
            hhhhh.write("parm '"+str(parm)+"'\n")
            hhhhh.write("low '"+str(low)+"'\n")
            hhhhh.write("hi '"+str(hi)+"'\n")
            hhhhh.write(json.dumps(basemeta, indent=4)+"\n")
            hhhhh.write("About to call updateDefaultValues()\n")

        # Change the default for Maximum Depth Below Flood Stage
        try:
            basemeta = self.updateDefaultValues(basemeta, "maxDepthBelow", -5)
            if traceNow :
                hhhhh.write("Updated metadata:\n")
                hhhhh.write(json.dumps(basemeta, indent=4)+"\n")
        except:
            if traceNow :
                hhhhh.write(traceback.format_exc()+"\n")
            else :
                traceback.print_exc()
        if traceNow :
            hhhhh.close()

        return basemeta


There are several things going on here that are unique in comparison to other customization solutions shown in this document. Instead of completely replacing the existing method for obtaining the metadata, we directly call the method in the base class CommonMetaData to get an initial version of the metadata, and then modify that resulting metadata. This has the advantage that if a new base version of CommonMetaData::getStageWindow() appears in a future release, there is a very good chance that no manual merge will be required. We include in the method a simple way for someone to optionally track the behavior of this new version of getStageWindow() in a temporary file. This is set up so that if there is a runtime error in the method updateDefaultValues(), the traceback will also end up in that temporary file. This can be helpful because it then becomes possible for someone not working in eclipse to nonetheless debug the behavior of updateDefaultValues() method. Even if one is working in eclipse, it can sometime be very difficult to identify one traceback of interest from the 'firehose' of information that can appear in the eclipse console.

3.2.6 Change precision for additional rainfall selectors.     contents    ^  v   

In the Hazard Information Dialog for convective Flash Flood Warnings, there are selectors for specifying additional rain that is expected to fall. The current default behavior of those selectors is for them to only allow rainfall to even whole inches. This customization example explains how to change the precision allowed for these selectors. First it is necessary to identify the module where the default behavior for this is invoked. We use the labeling string for those selectors and search the metadata modules using these commands:

> cd /awips2/edex/data/utility/common_static/base/HazardServices/
> touch /tmp/bbb
> find . -name '*Meta*py' -exec grep -i 'additional rain' '{}' /tmp/bbb \;

This tells us that the default megawidget specification for this menu is in CommonMetaData.py. Inspecting the base version of CommonMetaData.py tells us that the method that puts together this megawidget specification is called additionalRain(). We could override this method for CommonMetaData.py, which would change this behavior for all product types. For this example we choose to implement an override for additionalRain() in MetaData_FF_W_Convective.py, so that the change only applies to that hazard type. What follows is the override for MetaData_FF_W_Convective.py, with changes in
green:

class MetaData(CommonMetaData.MetaData):

    def additionalRain(self):
        return  {"identifier":"addtlRain",
                 "displayString": "Additional rainfall",
                 "productString":
                    '''Additional rainfall amounts of |* additionalRainLowerBound *| to |* additionalRainUpperBound *| inches are possible in the
                    warned area.''',
                 "detailFields": [
                        {
                             "fieldType": "FractionSpinner",
                             "fieldName": "additionalRainLowerBound",
                             "label": "of",
                             "sendEveryChange": False,
                             "minValue": 0,
                             "maxValue": 99,
                             "values": 0,
                             "incrementDelta": 0.25,
                             "precision": 2
                        },
                        {
                             "fieldType": "FractionSpinner",
                             "fieldName": "additionalRainUpperBound",
                             "label": " to",
                             "sendEveryChange": False,
                             "minValue": 0,
                             "maxValue": 99,
                             "values": 0,
                             "incrementDelta": 0.25,
                             "precision": 2
                        },
                        {
                             "fieldType": "Label",
                             "fieldName": "additionalRainSuffix",
                             "values": "inches is expected"
                        }
                       ]
                      }

With this override, we specify 2 digit precision, which means it is possible to specify rainfall values to the nearest 0.01 inches. This degree of precision, which is unrealistic for any forecast rainfall amount, is nonetheless required to enable the incrementDelta value of a quarter inch. One could make the argument that it makes no sense to increase the precision of the additional rainfall specifier without also doing the same for the ‘rain so far’ specifier; this is left as an exercise for the user.

3.2.7 Interdependency scripts.     contents    ^  ─  + v   

Interdependency scripts are a means by which a choice made by a user in the Hazard Information Dialog can result in automatically changing the state of other elements in the Hazard Information Dialog. While interdependency scripts are the most efficient way to accomplish this, they are not the only way. One can also define the "refreshMetadata" key to be True for a megawidget element, and this will result in the execute() method for the metadata to be rerun when that element is changed. (To see an example of invoking this refresh logic, examine the getHydrologicCause() method of MetaData_FF_W_NonConvective.py.) Also note that while interdependency scripts can result in changing the state of other elements, invoking the refresh logic is the only way for one choice to result in adding, removing, or changing the label of some other element.  Also, one does not have access to the hazard event inside an interdependency script.

Note that the method applyInterdependencies() is a static method of metadata modules. This particular static module method is overridable, but only because the logic that executes the metadata python is hardwired to do so. Therefore, be advised that it is not possible to introduce any additional static module methods in any of the metadata modules through override. Also be aware that many instances of applyInterdependencies() start with something similar to the following:

def applyInterdependencies(triggerIdentifiers, mutableProperties):
    propertyChanges = CommonMetaData.applyInterdependencies(triggerIdentifiers, mutableProperties)

The exact name of the module from CommonMetaData may be different than applyInterdependencies.  The thing to keep in mind is if you have a similar construct in an override MetaData module, then at times one must put import CommonMetaData at the top of the override file.

Under the next four headings are examples of using an interdependency script. There is more information about interdependency scripts in the megawidget document.

3.2.7.1 Workaround for problems with floating point spinners.   contents    ^  ─  + v   

One common feature of the metadata for areal flood hazards is the use of floating point spinners to enter observed or expected rainfall amounts. Ideally these spinners would also allow the user to type in a number, but that feature has never worked well.  This customization example leverages an interdependency script to pair floating point spinners with a text entry field to at least obtain the desired functionality. Here we apply this to the Rain so far: entries for the convective FFW; it is left as an exercise for the reader to apply this to other situations if desired. Note that the extra 30 spaces added to the "inches of rain have fallen" label is to avoid annoying extra whitespace after the Text widgets.


MetaData_FF_W_Convective.py:

class MetaData(CommonMetaData.MetaData):

    def enterAmount(self):
        return  {"identifier":"rainKnown", "displayString":"Between",
                 "detailFields": [
                    {
                        "fieldType": "FractionSpinner",
                        "fieldName": "rainSoFarLowerBound",
                        "sendEveryChange": False,
                        "minValue": 0,
                        "maxValue": 99,
                        "values": 0,
                        "incrementDelta": 1,
                        "precision": 1
                    },
                    {
                        "fieldType": "Text",
                        "fieldName": "rainSoFarLowerBoundTextMirror",
                        "maxChars": 5,
                        "label": "<>",
                        "values": "0"
                    },

                    {
                        "fieldType": "FractionSpinner",
                        "fieldName": "rainSoFarUpperBound",
                        "label": " and",
                        "sendEveryChange": False,
                        "minValue": 0,
                        "maxValue": 99,
                        "values": 0,
                        "incrementDelta": 1,
                        "precision": 1
                    },
                    {
                        "fieldType": "Text",
                        "fieldName": "rainSoFarUpperBoundTextMirror",
                        "maxChars": 5,
                        "label": "<>",
                        "values": "0"
                    },

                    {
                        "fieldType": "Label",
                        "fieldName": "rainAmtSuffix",
                        "values": "inches of rain have fallen"+(" "*30)
                    }
                 ]
                }

def applyInterdependencies(triggerIdentifiers, mutableProperties):
    propertyChanges = CommonMetaData.applyInterdependencies(triggerIdentifiers, mutableProperties)

    # Only invocations with a single trigger indicate HID edits.
    if not triggerIdentifiers :
        return propertyChanges
    if len(triggerIdentifiers)!=1:
        return propertyChanges
    triggerId = str(triggerIdentifiers[0])

    if not triggerId in ["rainSoFarLowerBound", "rainSoFarLowerBoundTextMirror",
                         "rainSoFarUpperBound", "rainSoFarUpperBoundTextMirror", ] :
        return propertyChanges
    propertyDict = mutableProperties.get(triggerId)
    if not propertyDict :
        return propertyChanges
    dictValue = propertyDict.get("values")
    if not dictValue :
        return propertyChanges
    dictValue = str(dictValue)

    if triggerId.find("TextMirror")>0 :
        try :
            floatval = float(dictValue)
        except :
            return propertyChanges
        propertyChanges = {}
        propertyChanges[triggerId.replace("TextMirror","")] = { "values" : floatval }
    else :
        propertyChanges = {}
        propertyChanges[triggerId+"TextMirror"] = { "values" : dictValue }
    return propertyChanges


3.2.7.2 Changing other default state based on the Source selection.
            
contents    ^    + v   

This example focuses on the state of a checkbox labeled Flash Flooding occurring that appears in the Hazard Information Dialog for convective Flash Flood warnings. Here the goal is to change its default value depending on which value is selected for the set of radio buttons under Source:. If the user actually changes the state of the Flash Flooding occurring checkbox from the Hazard Information Dialog, thereafter the user’s choice for the checkbox will be honored and this defaulting logic will be disabled.

The override that follows for MetaData_FF_W_Convective.py contains an interdependency script that invokes this behavior:

import copy

def applyInterdependencies(triggerIdentifiers, mutableProperties):
    propertyChanges = CommonMetaData.applyInterdependencies(triggerIdentifiers, mutableProperties)

    # Make sure we have access to the mutable info for source and flashFlood.
    # source is our basis reporting source, flashFlood is for whether flash
    # flooding is occurring immediately.
    try:
        flashFloodMutable = mutableProperties['flashFlood']
        sourceMutable = mutableProperties['source']
    except:
        return propertyChanges

    # If the flashFlood selector has already been changed by user, leave it alone.
    try :
        flashFloodSelected = flashFloodMutable['extraData']['chosen']
    except :
        flashFloodSelected = False
    if flashFloodSelected :
        return propertyChanges

    # If the flashFlood selector was just changed by the user, mark it as such.
    if triggerIdentifiers :
        if 'flashFlood' in triggerIdentifiers and len(triggerIdentifiers)==1 :
            flashFloodChanges = copy.deepcopy(flashFloodMutable)
            flashFloodChanges['extraData'] = { 'chosen' : True }
            propertyChanges = { 'flashFlood' : flashFloodChanges }
            return propertyChanges

    # If basis reporting source selector was NOT just changed by user, then
    # nothing to do.
    if not triggerIdentifiers :
        return propertyChanges
    if len(triggerIdentifiers)!=1 or not 'source' in triggerIdentifiers :
        return propertyChanges

    # Gather up current state of these selectors.
    try :
        sourceValue = sourceMutable['values']
        flashFloodValue = flashFloodMutable['values']
    except :
        return propertyChanges

    # If these states are already in sync, then nothing to do.
    desiredValue = sourceValue in [ "trainedSpottersSource", "publicSource", \
                                    "localLawEnforcementSource", "emergencyManagementSource" ]
    if desiredValue == flashFloodValue :
        return propertyChanges

    # Forcibly update state of flash flood occuring selector based on state of
    # the basis reporting source
    flashFloodChanges = copy.deepcopy(flashFloodMutable)
    flashFloodChanges['values'] = desiredValue
    propertyChanges = { 'flashFlood' : flashFloodChanges }

    return propertyChanges

3.2.7.3 Sharing methods between MetaData class and interdependency script.
            
contents    ^    + v   

There may be times when, in order to avoid duplicating code, it would be desirable to share methods between the code in a MetaData class and the code in an interdependency script. This is a bit tricky, because many of the typical classes used for broadly shared methods (such as TextProductCommon.py) are not importable by the MetaData modules. The module GeneralUtilities.py, which is importable by MetaData modules, is a reasonable place to put such shared methods. This module is simply a collection of static methods rather than a class.


Here will be one of the rare instances where we will demonstrate a point with an override that does nothing that is actually useful; this is just an example of how to structure the code such that a MetaData class and an interdependency script can share a method.  This override example will function, but will only output the file /tmp/testSharedMethod.txt to demonstrate the method sharing; it changes no actual behavior for the user.

GeneralUtilities.py:

import traceback
import time
import datetime

def testSharedMethod():
    stamp = str(datetime.datetime.fromtimestamp(time.time())).split(' ')[1][:11]
    ffff = open("/tmp/testSharedMethod.txt", "a")
    ffff.write(stamp+"\n")
    tbData = traceback.format_stack()
    for onetb in tbData[:-1] :
        ffff.write(onetb)
    ffff.write("\n")
    ffff.close()
    return None

MetaData_FA_Y.py:

import GeneralUtilities

class MetaData(CommonMetaData.MetaData):

    def execute(self, hazardEvent=None, metaDict=None):
        self.initialize(hazardEvent, metaDict)
        GeneralUtilities.testSharedMethod()
        if self.hazardStatus in ["ending", "ended", "elapsing", "elapsed"]:
            metaData = [
                        self.getEndingOption(),
                        self.getAdvisoryTypeHidden()
                        ]
        else: # issued/pending
            source = self.getSource()
            eventType = self.getEventType()
            self.ctaImpact = CallsToActionAndImpacts()
            metaData = [
                    self.getImmediateCause(),
                    source,
                    eventType,
                    self.getAdvisoryType(),
                    self.getOptionalSpecificType(),
                    self.getMinorFloodOccurring(),
                    self.getRainAmt(),
                    self.getLocationsAffected(),
                    self.lidImpactsList(),
                    self.getAdditionalInfo(notation="comments"),
                    self.getCTAs(self.hazardEvent.get("cta")),
                ]
            if hazardEvent:
                immediateCause = hazardEvent.get('immediateCause')
                if immediateCause == self.immediateCauseDR()['identifier']:
                    damOrLeveeName = hazardEvent.get('damOrLeveeName')
                    metaData.insert(2, self.getDamOrLevee(damOrLeveeName))

        return {
                HazardConstants.METADATA_KEY: metaData
                }

def applyInterdependencies(triggerIdentifiers, mutableProperties):
    GeneralUtilities.testSharedMethod()
    propertyChanges = CommonMetaData.applyInterdependencies(triggerIdentifiers, mutableProperties)
    return propertyChanges

3.2.7.4 Source, Event Type, and Immediate Cause interdependencies.
        
contents    ^    + v   

Here we are just going to refer to another document where this has been discussed in gory detail. This is because our plan is to formalize this functionality in a later release, and so this section will at a minimum change a great deal, and may possibly be temporary.


3.2.8 Disclaimers that clarify what is editable.     contents    ^    + v   

A Hazard Services ticket (#45995) has been written to address some confusing aspects of the appearance of the Impact Statement tab in the Hazard Information Dialog for River hazards. The problem is that the individual impact statements appear to be editable in the Hazard Information Dialog but are not. It may be some time before this ticket is completed, but in the mean time we present this customization that shows disclaimers that make it clear what is editable and what is not. The file being overridden is CommonMetaData.py. This customization is large because it overrides some methods with alot of code, but the changes themselves (in the customary green) are very simple.

class MetaData(object):

    def getRiseCrestFall(self):
        return [
                {
                 "fieldName": "riseAbove",
                 "fieldType": "HiddenField"
                 },
                {
                 "fieldName": "crest",
                 "fieldType": "HiddenField"
                 },
                {
                 "fieldName": "fallBelow",
                 "fieldType": "HiddenField"
                 },
                {
                 "fieldType": "Label",
                 "fieldName": "userInfo",
                 "values": "Note: Parameters shown are for user reference and are uneditable.",
                 "italic": True,
                 "bold": True
                },
                {
                 "fieldName": "riseAboveDescription",
                 "fieldType": "Text",
                 "label": "Rise Above Time:",
                 "visibleChars": 18,
                 "spacing": 5,
                 "editable": False,
                 "interdependencyOnly": True,
                 "readOnly": True,
                 },
                {
                 "fieldName": "crestDescription",
                 "fieldType": "Text",
                 "label": "Crest Time:",
                 "visibleChars": 18,
                 "spacing": 2,
                 "editable": False,
                 "interdependencyOnly": True,
                 "readOnly": True,
                 },
                {
                 "fieldName": "fallBelowDescription",
                 "fieldType": "Text",
                 "label": "Fall Below Time:",
                 "visibleChars": 18,
                 "spacing": 2,
                 "editable": False,
                 "interdependencyOnly": True,
                 "readOnly": True,
                 },
                {
                 "fieldName": "editRiseCrestFallButtonComp",
                 "fieldType": "Composite",
                 "expandHorizontally": False,
                 "fields": [
                            {
                             "fieldType": "Button",
                             "fieldName": "riseCrestFallButton",
                             "label": " Graphical Time Editor... ",
                             "editRiseCrestFall": True
                             }
                            ]
                 }
                ]

    def getForecastPointsSection(self, parm):
        pointID = self.hazardEvent.get("pointID")

        if self.riverForecastUtils is None:
            self.riverForecastUtils = RiverForecastUtils()

        self.getRiverForecastPoint(pointID, True)
        PE = self.riverForecastPoint.getPhysicalElement()

        curObs = self.riverForecastUtils.getObservedLevel(self.riverForecastPoint)
        maxFcst = self.riverForecastUtils.getMaximumForecastLevel(self.riverForecastPoint, PE)

        if PE[0] == PE_H :
            # get flood stage
            fldStg = self.riverForecastPoint.getFloodStage()
            fldFlow = HazardConstants.MISSING_VALUE
        else :
            # get flood flow
            fldStg = HazardConstants.MISSING_VALUE
            fldFlow = self.riverForecastPoint.getFloodFlow()

        curObs = '{:<15.2f}'.format(curObs)
        maxFcst = '{:<15.2f}'.format(maxFcst)
        fldStg = '{:<15.2f}'.format(fldStg)
        fldFlow = '{:<15.2f}'.format(fldFlow)
        lookupPE = '{:15s}'.format(PE)
        basedOnLookupPE = self.basedOnLookupPE
        riverLabel = self.getRiverLabel(parm)

        userInfo = {
                      "fieldType": "Label",
                      "fieldName": parm+"UserInfo",
                      "values": "Note: Parameters shown are for user reference and are uneditable.",
                      "italic": True,
                      "bold": True
                      }
        curObsField = {
                      "fieldName": parm + "CurObsField",
                      "fieldType": "Text",
                      "label": "CurObs",
                      "values": curObs,
                      "readOnly":True,
                  }
        maxFcstField = {
                      "fieldName":  parm + "MaxFcstField",
                      "fieldType": "Text",
                      "label": "MaxFcst",
                      "values": maxFcst,
                      "readOnly":True,
                  }
        fldStgField = {
                      "fieldName":  parm + "FldStgField",
                      "fieldType": "Text",
                      "label": "FldStg",
                      "values": fldStg,
                      "readOnly":True,
                  }
        fldFlowField = {
                      "fieldName":  parm + "FldFlowField",
                      "fieldType": "Text",
                      "label": "FldFlow",
                      "values": fldFlow,
                      "readOnly":True,
                  }
        lookupPEField = {
                      "fieldName":  parm + "LookupPEField",
                      "fieldType": "Text",
                      "label": "Lookup PE",
                      "values": lookupPE,
                      "readOnly":True,
                  }
        basedOnLookupPEField = {
                      "fieldName":  parm + "BasedOnLookupPEField",
                      "fieldType": "Text",
                      "label": "Based On Lookup PE",
                      "values": basedOnLookupPE,
                      "readOnly":True,
                  }

        group = {
                 "fieldType": "Group",
                 "fieldName": parm + "ForecastPointsGroup",
                 "expandHorizontally": False,
                 "expandVertically": True,
                 "fields" : [riverLabel, userInfo, curObsField, maxFcstField, fldStgField, fldFlowField, lookupPEField, basedOnLookupPEField]
                 }
        return group

    def getSelectedForecastPoints(self, parm):
        pointID = self.hazardEvent.get("pointID")
        if self.riverForecastUtils is None:
            self.riverForecastUtils = RiverForecastUtils()

        self.getRiverForecastPoint(pointID, True)
        primaryPE = self.riverForecastPoint.getPhysicalElement()
        filterValues = self.crestsAndImpactsUtil.getSearchParameterFilterValues(parm, self.hazardEvent, self.riverForecastPoint)

        impactsTextField = None
        if parm == "impacts":
            headerLabel = "Impacts to Use"
            selectionLabel = "ImpactStg/Flow - Start - End - Tendency"

            simTimeMils = SimulatedTime.getSystemTime().getMillis()
            currentTime = datetime.datetime.utcfromtimestamp(simTimeMils / 1000)

            impactDataList = self.riverForecastUtils.getImpactsDataList(pointID, currentTime.month, currentTime.day)
            plist = JUtilHandler.javaCollectionToPyCollection(impactDataList)

            characterizations, physicalElements, descriptions = self.riverForecastUtils.getImpacts(plist, primaryPE, self.riverForecastPoint, filterValues)
            values = self.crestsAndImpactsUtil.makeImpactTextFieldKeys(characterizations, physicalElements)
            impactChoices = self.makeImpactsChoices(characterizations, physicalElements, descriptions)
            currentValues = self.hazardEvent.get("impactCheckBoxes", None)
            if self.metaDict.get('refreshMetadataKey') == 'impactsApplyButton' or currentValues == None:
                checkedValues = self.crestsAndImpactsUtil.getSelectedImpacts(self.hazardEvent, self.riverForecastPoint)
            else:
                checkedValues = currentValues
            selectedForecastPoints = {
                                      "fieldType":"CheckBoxes",
                                      "fieldName": "impactCheckBoxes",
                                      "label": "Impacts",
                                      "choices": self.sortImpactsChoices(impactChoices, values),
                                      "values" : checkedValues,
                                      "extraData" : { "origList" : checkedValues},
                                      "useNewValueOnRefresh": True,
                                      }
            userInfo2 = {
                      "fieldType": "Label",
                      "fieldName": parm+"UserInfo2",
                      "values": \
            "Note: All following text is for user reference; edits must be made in Product Editor.",
                      "italic": True,
                      "bold": True
                      }
        else:
            userInfo2 = None
            headerLabel = "Crest to Use"
            selectionLabel = "CrestStg/Flow - CrestDate"
            defCrest, crestList = self.riverForecastUtils.getHistoricalCrest(self.riverForecastPoint, primaryPE, filterValues)

            if defCrest.startswith(HazardConstants.MISSING_VALUE_STR):
                defCrest = ""
                crestList.append("")
            choices = crestList

            currentValue = self.hazardEvent.get("crestsSelectedForecastPointsComboBox", None)
            if self.metaDict.get('refreshMetadataKey') == 'crestsApplyButton' or currentValue == None:
                value = defCrest
            else:
                value = currentValue

            selectedForecastPoints = {
                    "fieldType": "ComboBox",
                    "fieldName": parm + "SelectedForecastPointsComboBox",
                    "choices": choices,
                    "values": value,
                    "expandHorizontally": False,
                    "expandVertically": True,
                    "useNewValueOnRefresh": True
            }

        groupHeaderLabel = {
                       "fieldType": "Label",
                       "fieldName": parm + "GroupForecastPointsLabel",
                       "leftMargin": 40,
                       "rightMargin": 10,
                       "values": headerLabel,
                       }

        selectionHeaderLabel = {
                       "fieldType": "Label",
                       "fieldName": parm + "SelectedForecastPointsLabel",
                       "values": selectionLabel,
                       }

        if userInfo2 :
            fields = [ groupHeaderLabel,selectionHeaderLabel,userInfo2,selectedForecastPoints ]
        else :
            fields = [ groupHeaderLabel, selectionHeaderLabel, selectedForecastPoints ]

        grp = {
            "fieldName": parm + "PointsAndTextFieldGroup",
            "fieldType":"Group",
            "label": "",
            "expandHorizontally": False,
            "expandVertically": True,
            "fields": fields
            }
        return grp

    def makeImpactsChoices(self, characterizations, physicalElements, descriptions):
        choices = []
        zipped = zip(characterizations, physicalElements, descriptions)
        zipped.sort()
        for char, pe, desc in zipped :
            dispStr = str(char).replace("01/01-12/31","All year")
            entry = {
                     "identifier": "impactCheckBox_" + pe + '_' + char,
                     "displayString": dispStr,
                     "physicalElement": pe,
                    "detailFields": [
                                     {
                                     "fieldType": "Text",
                                     "fieldName": "impactTextField_" + str(char),
                                     "expandHorizontally": False,
                                     "visibleChars": 35,
                                     "lines":2,
                                     "values": desc,
                                     "readOnly":True,
                                     }
                                   ]
                     }
            choices.append(entry)
        return choices

3.2.9 Showing Type Source on the Hazard Information Dialog.     contents    ^  v   

This only applies to river flood hazards. Issuing a river flood hazard depends on having observed and forecast stage or flow data for a gauge. Most gauges have multiple potential sources of both observed and forecast stage and/or flow data. These are referred to as a “Type Source”, and by default these are always shown in the Graphical Time Editor.  This override allows this information to also be shown directly on the Hazard Information Dialog. This involves very simple overrides to four modules; CommonMetaData.py, MetaData_FL_A.py,   MetaData_FL_W.py, and  MetaData_FL_Y.py.  Here we only show the overrides to CommonMetaData.py and MetaData_FL_A.py, with changes from base in the customary green. The overrides for MetaData_FL_W.py and  MetaData_FL_Y.py are so completely analogous to the override for MetaData_FL_A.py that we leave those as an exercise for the reader.

CommonMetaData.py:

class MetaData(object):

    def getTypeSource(self):
        typeSourceObs = self.hazardEvent.get("recommendationTypeSourceObs")
        typeSourceFcst = self.hazardEvent.get("recommendationTypeSourceFcst")
        if typeSourceObs or typeSourceFcst :
            if not typeSourceObs :
                typeSourceObs = 'na'
            if not typeSourceFcst :
                typeSourceFcst = 'na'
            labelStr = "Observed|Forecast type source:  "+typeSourceObs+" | "+typeSourceFcst
        else :
            labelStr = "No type source information."
        return {
            "fieldName": "TypeSource",
            "fieldType": "Label",
            "values": labelStr,
            }

MetaData_FL_A.py:

class MetaData(CommonMetaData.MetaData):
   
    def execute(self, hazardEvent=None, metaDict=None):
        self.initialize(hazardEvent, metaDict)
        self.basedOnLookupPE = '{:15s}'.format('YES')
        pointID = self.hazardEvent.get("pointID", "")
        self.getRiverForecastPoint(pointID, True)

        metaData = []
        if self.hazardStatus in ["ending", "ended", "elapsing", "elapsed"]:
            metaData.append(self.getRiverLabel())
            if not self.riverForecastPoint:
                metaData.append(self.getMissingRFPLabel("pointDetails"))
            metaData.append(self.getFloodPointTable())
        else:
            pointDetails = []
            pointDetails.append(self.getRiverLabel())
            if not self.riverForecastPoint:
                pointDetails.append(self.getMissingRFPLabel("pointDetails"))
            pointDetails += [
                            self.getImmediateCause(),
                            self.getTypeSource(),
                            self.getFloodPointTable(),
                            ]
            pointDetails.extend(self.getRiseCrestFall())

            # Impacts require the Hydro DB to contain information
            # for this specific point. Check for this and if the data
            # isn't there add a warning to the HID.
            impacts = []
            if self.riverForecastPoint:
                impacts = [self.getCrestsOrImpacts("impacts")]
            else:
                impacts = [self.getMissingRFPLabel("impacts")]

            metaData = [
                           {
                    "fieldType": "TabbedComposite",
                    "fieldName": "FLWTabbedComposite",
                    "leftMargin": 10,
                    "rightMargin": 10,
                    "topMargin": 10,
                    "bottomMargin": 10,
                    "expandHorizontally": True,
                    "expandVertically": True,
                    "pages": [
                                  {
                                    "pageName": "Point Details",
                                    "pageFields": pointDetails
                                   },
                                  {
                                    "pageName": "Impact Statement",
                                    "pageFields": impacts
                                   }
                            ]
                    },
                    # Megawidget state flag must be defined in order to add the
                    # applySideEffects flag to applyInterdependencies' returnDict.
                    {
                        "fieldName": "applySideEffects",
                        "fieldType": "HiddenField",
                    },
               ]
        return {
                HazardConstants.METADATA_KEY: metaData
                }

3.2.10 Including gauge data in areal flood hazards.     contents    ^  v    

Hazard Services has functionality that allows one to include river gauge data in areal flood hazards.  Typically, river gauge data is only shown in river hazard products, so this functionality is deactivated by default.  However, it is very simple to activate through the following override of CommonMetaData.py:

class MetaData(object):

    def riverInfoInFA(self):
        return True

This results in radio buttons labeled River Information and With Impacts being added in the Locations Affected section of the Hazard Information Dialog for areal flood hazards.  Activating the River Information radio button populates the list of Gauges to Include with any for which either the gauge itself or its reach polygon overlaps the area of the hazard. Activating the With Impacts radio button creates a hierarchical choice list below the Locations Affected section; this hierarchical choice list allows one to select gauges and impacts to mention for the gauge.

There are two other methods one can override that are binary switches controlling other aspects of this capability. These are hideMissingDataGauges() in CommonMetaData.py and mentionMissingDataGauges() in SectionLevelMethods.py.  It is left to the reader to read the comments on the Base level instances of these methods to learn what they do.


Also note that this capability does not currently work correctly for discharge gauges. Ticket #73784 is being worked to address this, but it will be some time before this fix appears formally in a Hazard Services release. Current thinking is that this will not be something of broad interest, so we will not include a patch here.  Put a question on the hazard services list server if this patch is needed.