ssscoring.appcommon
Common functions, classes, and objects to all Streamlit apps in the SSScoring package.
1# See: https://github.com/pr3d4t0r/SSScoring/blob/master/LICENSE.txt 2 3""" 4Common functions, classes, and objects to all Streamlit apps in the SSScoring 5package. 6""" 7 8from importlib_resources import files 9from io import StringIO 10 11from ssscoring import __VERSION__ 12from ssscoring.calc import isValidMaximumAltitude 13from ssscoring.calc import isValidMinimumAltitude 14from ssscoring.constants import DEFAULT_PLOT_INCREMENT 15from ssscoring.constants import DEFAULT_PLOT_MAX_V_SCALE 16from ssscoring.constants import DZ_DIRECTORY 17from ssscoring.constants import FLYSIGHT_FILE_ENCODING 18from ssscoring.constants import M_2_FT 19from ssscoring.constants import RESOURCES 20from ssscoring.datatypes import JumpResults 21from ssscoring.datatypes import JumpStatus 22from ssscoring.errors import SSScoringError 23from ssscoring.notebook import SPEED_COLORS 24from ssscoring.notebook import graphAltitude 25from ssscoring.notebook import graphAngle 26from ssscoring.notebook import graphJumpResult 27from ssscoring.notebook import initializeExtraYRanges 28from ssscoring.notebook import initializePlot 29 30import os 31 32import bokeh.models as bm 33import pandas as pd 34import pydeck as pdk 35import streamlit as st 36 37 38# *** constants *** 39 40DEFAULT_DATA_LAKE = './data' 41""" 42Default data lake directory when reading files from the local file system. 43""" 44 45STREAMLIT_SIG_KEY = 'HOSTNAME' 46""" 47Environment key used by the Streamlig.app environment when running an 48application. 49""" 50 51STREAMLIT_SIG_VALUE = 'streamlit' 52""" 53Expected value associate with the environment variable `STREAMLIT_SIG_KEY` when 54running in a Streamlit.app environment. 55""" 56 57 58# *** implementation *** 59 60def isStreamlitHostedApp() -> bool: 61 """ 62 Detect if the hosting environment is a native Python system or a Streamlit 63 app environment hosted by streamlit.io or Snowflake. 64 65 Returns 66 ------- 67 `True` if the app is running in the Streamlit app or Snoflake app 68 environment, otherwise `False`. 69 """ 70 keys = tuple(os.environ.keys()) 71 if STREAMLIT_SIG_KEY not in keys: 72 return False 73 if os.environ[STREAMLIT_SIG_KEY] == STREAMLIT_SIG_VALUE: 74 return True 75 return False 76 77 78@st.cache_data 79def fetchResource(resourceName: str) -> StringIO: 80 """ 81 Fetch a file-like resource from the `PYTHONPATH` and `resources` module 82 included in the SSScore package. Common resources include the drop zones 83 list CSV and in-line documentation Markdown text. 84 85 Arguments 86 --------- 87 resourceName 88 A string representing the resource file name, usually a CSV file. 89 90 Returns 91 ------- 92 An instance of `StringIO` ready to be process as a text stream by the 93 caller. 94 95 Raises 96 ------ 97 `SSScoringError` if the resource dataframe isn't the global drop zones 98 directory or the file is invalid in any way. 99 """ 100 try: 101 return StringIO(files(RESOURCES).joinpath(resourceName).read_bytes().decode(FLYSIGHT_FILE_ENCODING)) 102 except Exception as e: 103 raise SSScoringError('Invalid resource - %s' % str(e)) 104 105 106def initDropZonesFromResource() -> pd.DataFrame: 107 """ 108 Get the DZs directory from a CSV enclosed in the distribution package as a 109 resource. The resources package is fixed to `ssscoring.resources`, the 110 default resource file is defined by `DZ_DIRECTORY` but can be anything. 111 112 Returns 113 ------- 114 The global drop zones directory as a dataframe. 115 116 Raises 117 ------ 118 `SSScoringError` if the resource dataframe isn't the global drop zones 119 directory or the file is invalid in any way. 120 """ 121 try: 122 buffer = fetchResource(DZ_DIRECTORY) 123 dropZones = pd.read_csv(buffer, sep=',') 124 except Exception as e: 125 raise SSScoringError('Invalid resource - %s' % str(e)) 126 127 if 'dropZone' not in dropZones.columns: 128 raise SSScoringError('dropZone object is a dataframe but not the drop zones directory') 129 130 return dropZones 131 132 133def displayJumpDataIn(resultsTable: pd.DataFrame): 134 """ 135 Display the individual results, as a table. 136 137 Arguments 138 --------- 139 resultsTable: pd.DataFrame 140 The results from a speed skydiving jump. 141 142 See 143 --- 144 `ssscoring.datatypes.JumpResults` 145 """ 146 if resultsTable is not None: 147 table = resultsTable.copy() 148 table.vKMh = table.vKMh.apply(lambda x: round(x, 2)) 149 table.hKMh = table.hKMh.apply(lambda x: round(x, 2)) 150 table.deltaV = table.deltaV.apply(lambda x: round(x, 2)) 151 table.deltaAngle = table.deltaAngle.apply(lambda x: round(x, 2)) 152 table['altitude (ft)'] = table['altitude (ft)'].apply(lambda x: round(x, 1)) 153 table.index = ['']*len(table) 154 st.dataframe(table, hide_index=True) 155 156 157def interpretJumpResult(tag: str, 158 jumpResult: JumpResults, 159 processBadJump: bool): 160 """ 161 Interpret the jump results and generate the corresponding labels and 162 warnings if a jump is invalid, or only "somewhat valid" according to ISC 163 rules. The caller turns results display on/off depending on training vs 164 in-competition settins. Because heuristics are a beautiful thing. 165 166 Arguments 167 --------- 168 tag 169 A string that identifies a specific jump and the FlySight version that 170 generated the corresponding track file. Often in the form: `HH-mm-ss:vX` 171 where `X` is the FlySight hardware version. 172 173 jumpResult 174 An instance of `ssscoring.datatypes.JumpResults` with jump data. 175 176 processBadJump 177 If `True`, generate end-user warnings as part of its results processing if 178 the jump is invalid because of ISC rules, but set the status to OK so that 179 the jump may be displayed. 180 181 Returns 182 ------- 183 A `tuple` of these objects: 184 185 - `jumpStatusInfo` - the jump status, in human-readable form 186 - `scoringInfo` - Max speed, scoring window, etc. 187 - `badJumpLegend` - A warning or error if the jump is invalid according to 188 ISC rules 189 - `jumpStatus` - An instance of `JumpStatus` that may have been overriden to 190 `OK` if `processBadJump` was set to `True` and the jump was invalid. Used 191 only for display. Use `jumpResult.status` to determine the actual result 192 of the jump from a strict scoring perspective. `jumpStatus` is used 193 for display override purposes only. 194 """ 195 maxSpeed = jumpResult.maxSpeed 196 window = jumpResult.window 197 jumpStatus = jumpResult.status 198 jumpStatusInfo = '' 199 if jumpResult.status == JumpStatus.WARM_UP_FILE: 200 badJumpLegend = '<span style="color: red">Warm up file or SMD ran out of battery - nothing to do<br>' 201 scoringInfo = '' 202 elif jumpResult.status == JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT: 203 badJumpLegend = '<span style="color: red">%s - RE-JUMP: speed accuracy exceeds ISC threshold<br>' % tag 204 scoringInfo = '' 205 else: 206 scoringInfo = 'Max speed = {0:,.0f}; '.format(maxSpeed)+('exit at %d m (%d ft)<br>Validation window starts at %d m (%d ft)<br>End scoring window at %d m (%d ft)<br>' % \ 207 (window.start, M_2_FT*window.start, window.validationStart, M_2_FT*window.validationStart, window.end, M_2_FT*window.end)) 208 if (processBadJump and jumpStatus != JumpStatus.OK and jumpStatus != JumpStatus.WARM_UP_FILE and jumpStatus != JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT) or jumpStatus == JumpStatus.OK: 209 jumpStatusInfo = '<span style="color: %s">%s jump - %s - %.02f km/h</span><br>' % ('green', tag, 'VALID', jumpResult.score) 210 belowMaxAltitude = isValidMaximumAltitude(jumpResult.data.altitudeAGL.max()) 211 badJumpLegend = None 212 if not isValidMinimumAltitude(jumpResult.data.altitudeAGL.max()): 213 badJumpLegend = '<span style="color: yellow"><span style="font-weight: bold">Warning:</span> exit altitude AGL was lower than the minimum scoring altitude<br>' 214 jumpStatus = JumpStatus.ALTITUDE_EXCEEDS_MINIMUM 215 if not belowMaxAltitude: 216 jumpStatusInfo = '<span style="color: %s">%s jump - %s - %.02f km/h</span><br>' % ('red', tag, 'INVALID', jumpResult.score) 217 badJumpLegend = '<span style="color: red"><span style="font-weight: bold">RE-JUMP:</span> exit altitude AGL exceeds the maximum altitude<br>' 218 jumpStatus = JumpStatus.ALTITUDE_EXCEEDS_MAXIMUM 219 return jumpStatusInfo, scoringInfo, badJumpLegend, jumpStatus 220 221 222def plotJumpResult(tag: str, 223 jumpResult: JumpResults): 224 """ 225 Plot the jump results including altitude, horizontal speed, time, etc. for 226 evaluation and interpretation. 227 228 Arguments 229 --------- 230 tag 231 A string that identifies a specific jump and the FlySight version that 232 generated the corresponding track file. Often in the form: `HH-mm-ss:vX` 233 where `X` is the FlySight hardware version. 234 235 jumpResult 236 An instance of `ssscoring.datatypes.JumpResults` with jump data. 237 """ 238 if jumpResult.data is not None: 239 try: 240 yMax = DEFAULT_PLOT_MAX_V_SCALE if jumpResult.score <= DEFAULT_PLOT_MAX_V_SCALE else jumpResult.score + DEFAULT_PLOT_INCREMENT 241 except TypeError: 242 yMax = DEFAULT_PLOT_MAX_V_SCALE 243 plot = initializePlot(tag, backgroundColorName='#2c2c2c', yMax=yMax) 244 plot = initializeExtraYRanges(plot, startY=min(jumpResult.data.altitudeAGLFt)-500.0, endY=max(jumpResult.data.altitudeAGLFt)+500.0) 245 graphAltitude(plot, jumpResult) 246 graphAngle(plot, jumpResult) 247 hoverValue = bm.HoverTool(tooltips=[('time', '@x{0.0}s'), ('y-val', '@y{0.00}')]) 248 plot.add_tools(hoverValue) 249 graphJumpResult(plot, jumpResult, lineColor=SPEED_COLORS[0]) 250 st.bokeh_chart(plot, use_container_width=True) 251 252 253def initFileUploaderState(filesObject:str, uploaderKey:str ='uploaderKey'): 254 """ 255 Initialize the session state for the Streamlit app uploader so that 256 selections can be cleared in callbacks later. 257 258 **Important**: `initFileUploaderState()` __must__ be called after setting the 259 page configuration (per Streamlit architecture rules) and before adding any 260 widgets to the sidebars, containers, or main application. 261 262 Argument 263 -------- 264 filesObject 265 A `str` name that is either `'trackFile'` for the single track file process 266 or `'trackFiles'` for the app page that handles more than one track file at 267 a time. 268 269 uploaderKey 270 A unique identifier for the uploader key component, usually set to 271 `'uploaderKey'` but can be any arbitrary name. This value must match the 272 `file_uploader(..., key=uploaderKey,...)` value. 273 274 """ 275 if filesObject not in st.session_state: 276 st.session_state[filesObject] = None 277 if uploaderKey not in st.session_state: 278 st.session_state[uploaderKey] = 0 279 280 281def displayTrackOnMap(deck: pdk.Deck, 282 displayScore=True): 283 """ 284 Displays a track map drawn using PyDeck. 285 286 Arguments 287 --------- 288 deck 289 A PyDeck initialized with map layers. 290 291 displayScore 292 If `True`, display the max score point; else display the max speed point. 293 """ 294 if deck is not None: 295 label = 'score' if displayScore else 'speed' 296 st.write('Brightest point shows the max **%s** point. Exit at orange point. Each track dot is 4 m in diameter.' % label) 297 st.pydeck_chart(deck) 298 299 300@st.dialog('DZ Coordinates') 301def displayDZCoordinates(): 302 """ 303 Display the DZ coordinates in a dialog if one is selected in the drop zones 304 selection box. The selection is stored in the `st.session_state.currentDropZone` 305 variable. If a valid name is available, the latitude and longitude are 306 displayed. If `None` or invalid, a notification dialog is displayed. 307 308 The corresponding UI button is only enabled if the user selected a valid DZ, 309 otherwise the button is disabled. 310 """ 311 dropZones = initDropZonesFromResource() 312 lat = float(dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].lat) 313 lon = float(dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].lon) 314 st.write(st.session_state.currentDropZone) 315 st.write('lat, lon: %.4f, %.4f' % (lat, lon)) 316 if st.button('OK'): 317 st.rerun() 318 319 320def setSideBarAndMain(icon: str, singleTrack: bool, selectDZState): 321 """ 322 Set all the interactive and navigational components for the app's side bar. 323 324 Arguments 325 --------- 326 icon 327 A meaningful Emoji associated with the the side bar's title. 328 329 singleTrack 330 A flag for allowing selection of a single or multiple track files in the 331 corresponding selector component. Determines whether the side bar is used 332 for the single- or multiple selection application. 333 334 selectDZState 335 A callback for the drop zone selector selection box, affected by events in 336 the main application. 337 338 Notes 339 ----- 340 All the aplication level values associated with the components and 341 selections from the side bar are stored in `st.session_state` and visible 342 across the whole application. 343 344 **Do not** cache calls to `ssscoring.appcommon.setSideBarAndMain()` because 345 this can result in unpredictable behavior since the cache may never be 346 cleared until application reload. 347 """ 348 dropZones = initDropZonesFromResource() 349 st.session_state.currentDropZone = None 350 elevation = None 351 st.sidebar.title('%s SSScore %s' % (icon, __VERSION__)) 352 st.session_state.processBadJump = st.sidebar.checkbox('Process bad jumps', value=True, help='Display results from invalid jumps') 353 st.session_state.currentDropZone = st.sidebar.selectbox('Select the drop zone:', dropZones.dropZone, index=None, on_change=selectDZState, disabled=(elevation != None and elevation != 0.0)) 354 elevation = st.sidebar.number_input('...or enter the DZ elevation in meters:', min_value=0.0, max_value=4000.0, value='min', format='%.2f', disabled=(st.session_state.currentDropZone != None), on_change=selectDZState) 355 if st.session_state.currentDropZone: 356 st.session_state.elevation = dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].elevation 357 elif elevation != None and elevation != 0.0: 358 st.session_state.elevation= elevation 359 else: 360 st.session_state.elevation = None 361 st.session_state.trackFiles = None 362 st.sidebar.metric('Elevation', value='%.1f m' % (0.0 if st.session_state.elevation == None else st.session_state.elevation)) 363 if singleTrack: 364 trackFile = st.sidebar.file_uploader('Track file', type=[ 'csv', ], disabled=st.session_state.elevation == None, key = st.session_state.uploaderKey) 365 if trackFile: 366 st.session_state.trackFile = trackFile 367 else: 368 trackFiles = st.sidebar.file_uploader( 369 'Track files', 370 type=[ 'csv', ], 371 disabled=st.session_state.elevation == None, 372 accept_multiple_files=True, 373 key = st.session_state.uploaderKey 374 ) 375 if trackFiles: 376 st.session_state.trackFiles = trackFiles 377 st.sidebar.button('Clear', on_click=selectDZState) 378 st.sidebar.button('Display DZ coordinates', on_click=displayDZCoordinates, disabled=(st.session_state.currentDropZone == None)) 379 st.sidebar.link_button('Report missing DZ', 'https://github.com/pr3d4t0r/SSScoring/issues/new?template=report-missing-dz.md', icon=':material/breaking_news_alt_1:') 380 st.sidebar.link_button('Feature request or bug report', 'https://github.com/pr3d4t0r/SSScoring/issues/new?template=Blank+issue', icon=':material/breaking_news_alt_1:') 381 382 383def setSideBarDeprecated(icon: str): 384 """ 385 Set a disabled version of the sidebar to maintain UX compatibility with the 386 actual app, used when the main screen is configured to display an end-user 387 message like "we moved to a new domain." 388 389 Arguments 390 --------- 391 icon 392 A meaningful Emoji associated with the the side bar's title. 393 """ 394 dropZones = initDropZonesFromResource() 395 st.session_state.currentDropZone = None 396 elevation = None 397 st.sidebar.title('%s SSScore %s' % (icon, __VERSION__)) 398 st.session_state.processBadJump = st.sidebar.checkbox('Process bad jumps', value=True, help='Display results from invalid jumps', disabled=True) 399 st.session_state.currentDropZone = st.sidebar.selectbox('Select the drop zone:', dropZones.dropZone, index=None, disabled=True) 400 elevation = st.sidebar.number_input('...or enter the DZ elevation in meters:', min_value=0.0, max_value=4000.0, value='min', format='%.2f', disabled=True) 401 if st.session_state.currentDropZone: 402 st.session_state.elevation = dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].elevation 403 elif elevation != None and elevation != 0.0: 404 st.session_state.elevation= elevation 405 else: 406 st.session_state.elevation = None 407 st.session_state.trackFiles = None 408 st.sidebar.metric('Elevation', value='%.1f m' % (0.0 if st.session_state.elevation == None else st.session_state.elevation)) 409 trackFile = st.sidebar.file_uploader('Track file', type=[ 'csv', ], disabled=True) 410 st.sidebar.button('Clear', disabled=True) 411 st.sidebar.button('Display DZ coordinates', disabled=True)
Default data lake directory when reading files from the local file system.
Environment key used by the Streamlig.app environment when running an application.
Expected value associate with the environment variable STREAMLIT_SIG_KEY
when
running in a Streamlit.app environment.
61def isStreamlitHostedApp() -> bool: 62 """ 63 Detect if the hosting environment is a native Python system or a Streamlit 64 app environment hosted by streamlit.io or Snowflake. 65 66 Returns 67 ------- 68 `True` if the app is running in the Streamlit app or Snoflake app 69 environment, otherwise `False`. 70 """ 71 keys = tuple(os.environ.keys()) 72 if STREAMLIT_SIG_KEY not in keys: 73 return False 74 if os.environ[STREAMLIT_SIG_KEY] == STREAMLIT_SIG_VALUE: 75 return True 76 return False
Detect if the hosting environment is a native Python system or a Streamlit app environment hosted by streamlit.io or Snowflake.
Returns
True
if the app is running in the Streamlit app or Snoflake app
environment, otherwise False
.
79@st.cache_data 80def fetchResource(resourceName: str) -> StringIO: 81 """ 82 Fetch a file-like resource from the `PYTHONPATH` and `resources` module 83 included in the SSScore package. Common resources include the drop zones 84 list CSV and in-line documentation Markdown text. 85 86 Arguments 87 --------- 88 resourceName 89 A string representing the resource file name, usually a CSV file. 90 91 Returns 92 ------- 93 An instance of `StringIO` ready to be process as a text stream by the 94 caller. 95 96 Raises 97 ------ 98 `SSScoringError` if the resource dataframe isn't the global drop zones 99 directory or the file is invalid in any way. 100 """ 101 try: 102 return StringIO(files(RESOURCES).joinpath(resourceName).read_bytes().decode(FLYSIGHT_FILE_ENCODING)) 103 except Exception as e: 104 raise SSScoringError('Invalid resource - %s' % str(e))
Fetch a file-like resource from the PYTHONPATH
and resources
module
included in the SSScore package. Common resources include the drop zones
list CSV and in-line documentation Markdown text.
Arguments
resourceName
A string representing the resource file name, usually a CSV file.
Returns
An instance of StringIO
ready to be process as a text stream by the
caller.
Raises
SSScoringError
if the resource dataframe isn't the global drop zones
directory or the file is invalid in any way.
107def initDropZonesFromResource() -> pd.DataFrame: 108 """ 109 Get the DZs directory from a CSV enclosed in the distribution package as a 110 resource. The resources package is fixed to `ssscoring.resources`, the 111 default resource file is defined by `DZ_DIRECTORY` but can be anything. 112 113 Returns 114 ------- 115 The global drop zones directory as a dataframe. 116 117 Raises 118 ------ 119 `SSScoringError` if the resource dataframe isn't the global drop zones 120 directory or the file is invalid in any way. 121 """ 122 try: 123 buffer = fetchResource(DZ_DIRECTORY) 124 dropZones = pd.read_csv(buffer, sep=',') 125 except Exception as e: 126 raise SSScoringError('Invalid resource - %s' % str(e)) 127 128 if 'dropZone' not in dropZones.columns: 129 raise SSScoringError('dropZone object is a dataframe but not the drop zones directory') 130 131 return dropZones
Get the DZs directory from a CSV enclosed in the distribution package as a
resource. The resources package is fixed to ssscoring.resources
, the
default resource file is defined by DZ_DIRECTORY
but can be anything.
Returns
The global drop zones directory as a dataframe.
Raises
SSScoringError
if the resource dataframe isn't the global drop zones
directory or the file is invalid in any way.
134def displayJumpDataIn(resultsTable: pd.DataFrame): 135 """ 136 Display the individual results, as a table. 137 138 Arguments 139 --------- 140 resultsTable: pd.DataFrame 141 The results from a speed skydiving jump. 142 143 See 144 --- 145 `ssscoring.datatypes.JumpResults` 146 """ 147 if resultsTable is not None: 148 table = resultsTable.copy() 149 table.vKMh = table.vKMh.apply(lambda x: round(x, 2)) 150 table.hKMh = table.hKMh.apply(lambda x: round(x, 2)) 151 table.deltaV = table.deltaV.apply(lambda x: round(x, 2)) 152 table.deltaAngle = table.deltaAngle.apply(lambda x: round(x, 2)) 153 table['altitude (ft)'] = table['altitude (ft)'].apply(lambda x: round(x, 1)) 154 table.index = ['']*len(table) 155 st.dataframe(table, hide_index=True)
Display the individual results, as a table.
Arguments
resultsTable: pd.DataFrame
The results from a speed skydiving jump.
See
158def interpretJumpResult(tag: str, 159 jumpResult: JumpResults, 160 processBadJump: bool): 161 """ 162 Interpret the jump results and generate the corresponding labels and 163 warnings if a jump is invalid, or only "somewhat valid" according to ISC 164 rules. The caller turns results display on/off depending on training vs 165 in-competition settins. Because heuristics are a beautiful thing. 166 167 Arguments 168 --------- 169 tag 170 A string that identifies a specific jump and the FlySight version that 171 generated the corresponding track file. Often in the form: `HH-mm-ss:vX` 172 where `X` is the FlySight hardware version. 173 174 jumpResult 175 An instance of `ssscoring.datatypes.JumpResults` with jump data. 176 177 processBadJump 178 If `True`, generate end-user warnings as part of its results processing if 179 the jump is invalid because of ISC rules, but set the status to OK so that 180 the jump may be displayed. 181 182 Returns 183 ------- 184 A `tuple` of these objects: 185 186 - `jumpStatusInfo` - the jump status, in human-readable form 187 - `scoringInfo` - Max speed, scoring window, etc. 188 - `badJumpLegend` - A warning or error if the jump is invalid according to 189 ISC rules 190 - `jumpStatus` - An instance of `JumpStatus` that may have been overriden to 191 `OK` if `processBadJump` was set to `True` and the jump was invalid. Used 192 only for display. Use `jumpResult.status` to determine the actual result 193 of the jump from a strict scoring perspective. `jumpStatus` is used 194 for display override purposes only. 195 """ 196 maxSpeed = jumpResult.maxSpeed 197 window = jumpResult.window 198 jumpStatus = jumpResult.status 199 jumpStatusInfo = '' 200 if jumpResult.status == JumpStatus.WARM_UP_FILE: 201 badJumpLegend = '<span style="color: red">Warm up file or SMD ran out of battery - nothing to do<br>' 202 scoringInfo = '' 203 elif jumpResult.status == JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT: 204 badJumpLegend = '<span style="color: red">%s - RE-JUMP: speed accuracy exceeds ISC threshold<br>' % tag 205 scoringInfo = '' 206 else: 207 scoringInfo = 'Max speed = {0:,.0f}; '.format(maxSpeed)+('exit at %d m (%d ft)<br>Validation window starts at %d m (%d ft)<br>End scoring window at %d m (%d ft)<br>' % \ 208 (window.start, M_2_FT*window.start, window.validationStart, M_2_FT*window.validationStart, window.end, M_2_FT*window.end)) 209 if (processBadJump and jumpStatus != JumpStatus.OK and jumpStatus != JumpStatus.WARM_UP_FILE and jumpStatus != JumpStatus.SPEED_ACCURACY_EXCEEDS_LIMIT) or jumpStatus == JumpStatus.OK: 210 jumpStatusInfo = '<span style="color: %s">%s jump - %s - %.02f km/h</span><br>' % ('green', tag, 'VALID', jumpResult.score) 211 belowMaxAltitude = isValidMaximumAltitude(jumpResult.data.altitudeAGL.max()) 212 badJumpLegend = None 213 if not isValidMinimumAltitude(jumpResult.data.altitudeAGL.max()): 214 badJumpLegend = '<span style="color: yellow"><span style="font-weight: bold">Warning:</span> exit altitude AGL was lower than the minimum scoring altitude<br>' 215 jumpStatus = JumpStatus.ALTITUDE_EXCEEDS_MINIMUM 216 if not belowMaxAltitude: 217 jumpStatusInfo = '<span style="color: %s">%s jump - %s - %.02f km/h</span><br>' % ('red', tag, 'INVALID', jumpResult.score) 218 badJumpLegend = '<span style="color: red"><span style="font-weight: bold">RE-JUMP:</span> exit altitude AGL exceeds the maximum altitude<br>' 219 jumpStatus = JumpStatus.ALTITUDE_EXCEEDS_MAXIMUM 220 return jumpStatusInfo, scoringInfo, badJumpLegend, jumpStatus
Interpret the jump results and generate the corresponding labels and warnings if a jump is invalid, or only "somewhat valid" according to ISC rules. The caller turns results display on/off depending on training vs in-competition settins. Because heuristics are a beautiful thing.
Arguments
tag
A string that identifies a specific jump and the FlySight version that
generated the corresponding track file. Often in the form: HH-mm-ss:vX
where X
is the FlySight hardware version.
jumpResult
An instance of ssscoring.datatypes.JumpResults
with jump data.
processBadJump
If True
, generate end-user warnings as part of its results processing if
the jump is invalid because of ISC rules, but set the status to OK so that
the jump may be displayed.
Returns
A tuple
of these objects:
jumpStatusInfo
- the jump status, in human-readable formscoringInfo
- Max speed, scoring window, etc.badJumpLegend
- A warning or error if the jump is invalid according to ISC rulesjumpStatus
- An instance ofJumpStatus
that may have been overriden toOK
ifprocessBadJump
was set toTrue
and the jump was invalid. Used only for display. UsejumpResult.status
to determine the actual result of the jump from a strict scoring perspective.jumpStatus
is used for display override purposes only.
223def plotJumpResult(tag: str, 224 jumpResult: JumpResults): 225 """ 226 Plot the jump results including altitude, horizontal speed, time, etc. for 227 evaluation and interpretation. 228 229 Arguments 230 --------- 231 tag 232 A string that identifies a specific jump and the FlySight version that 233 generated the corresponding track file. Often in the form: `HH-mm-ss:vX` 234 where `X` is the FlySight hardware version. 235 236 jumpResult 237 An instance of `ssscoring.datatypes.JumpResults` with jump data. 238 """ 239 if jumpResult.data is not None: 240 try: 241 yMax = DEFAULT_PLOT_MAX_V_SCALE if jumpResult.score <= DEFAULT_PLOT_MAX_V_SCALE else jumpResult.score + DEFAULT_PLOT_INCREMENT 242 except TypeError: 243 yMax = DEFAULT_PLOT_MAX_V_SCALE 244 plot = initializePlot(tag, backgroundColorName='#2c2c2c', yMax=yMax) 245 plot = initializeExtraYRanges(plot, startY=min(jumpResult.data.altitudeAGLFt)-500.0, endY=max(jumpResult.data.altitudeAGLFt)+500.0) 246 graphAltitude(plot, jumpResult) 247 graphAngle(plot, jumpResult) 248 hoverValue = bm.HoverTool(tooltips=[('time', '@x{0.0}s'), ('y-val', '@y{0.00}')]) 249 plot.add_tools(hoverValue) 250 graphJumpResult(plot, jumpResult, lineColor=SPEED_COLORS[0]) 251 st.bokeh_chart(plot, use_container_width=True)
Plot the jump results including altitude, horizontal speed, time, etc. for evaluation and interpretation.
Arguments
tag
A string that identifies a specific jump and the FlySight version that
generated the corresponding track file. Often in the form: HH-mm-ss:vX
where X
is the FlySight hardware version.
jumpResult
An instance of ssscoring.datatypes.JumpResults
with jump data.
254def initFileUploaderState(filesObject:str, uploaderKey:str ='uploaderKey'): 255 """ 256 Initialize the session state for the Streamlit app uploader so that 257 selections can be cleared in callbacks later. 258 259 **Important**: `initFileUploaderState()` __must__ be called after setting the 260 page configuration (per Streamlit architecture rules) and before adding any 261 widgets to the sidebars, containers, or main application. 262 263 Argument 264 -------- 265 filesObject 266 A `str` name that is either `'trackFile'` for the single track file process 267 or `'trackFiles'` for the app page that handles more than one track file at 268 a time. 269 270 uploaderKey 271 A unique identifier for the uploader key component, usually set to 272 `'uploaderKey'` but can be any arbitrary name. This value must match the 273 `file_uploader(..., key=uploaderKey,...)` value. 274 275 """ 276 if filesObject not in st.session_state: 277 st.session_state[filesObject] = None 278 if uploaderKey not in st.session_state: 279 st.session_state[uploaderKey] = 0
Initialize the session state for the Streamlit app uploader so that selections can be cleared in callbacks later.
Important: initFileUploaderState()
__must__ be called after setting the
page configuration (per Streamlit architecture rules) and before adding any
widgets to the sidebars, containers, or main application.
Argument
filesObject
A str
name that is either 'trackFile'
for the single track file process
or 'trackFiles'
for the app page that handles more than one track file at
a time.
uploaderKey
A unique identifier for the uploader key component, usually set to
'uploaderKey'
but can be any arbitrary name. This value must match the
file_uploader(..., key=uploaderKey,...)
value.
282def displayTrackOnMap(deck: pdk.Deck, 283 displayScore=True): 284 """ 285 Displays a track map drawn using PyDeck. 286 287 Arguments 288 --------- 289 deck 290 A PyDeck initialized with map layers. 291 292 displayScore 293 If `True`, display the max score point; else display the max speed point. 294 """ 295 if deck is not None: 296 label = 'score' if displayScore else 'speed' 297 st.write('Brightest point shows the max **%s** point. Exit at orange point. Each track dot is 4 m in diameter.' % label) 298 st.pydeck_chart(deck)
Displays a track map drawn using PyDeck.
Arguments
deck
A PyDeck initialized with map layers.
displayScore
If True
, display the max score point; else display the max speed point.
301@st.dialog('DZ Coordinates') 302def displayDZCoordinates(): 303 """ 304 Display the DZ coordinates in a dialog if one is selected in the drop zones 305 selection box. The selection is stored in the `st.session_state.currentDropZone` 306 variable. If a valid name is available, the latitude and longitude are 307 displayed. If `None` or invalid, a notification dialog is displayed. 308 309 The corresponding UI button is only enabled if the user selected a valid DZ, 310 otherwise the button is disabled. 311 """ 312 dropZones = initDropZonesFromResource() 313 lat = float(dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].lat) 314 lon = float(dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].lon) 315 st.write(st.session_state.currentDropZone) 316 st.write('lat, lon: %.4f, %.4f' % (lat, lon)) 317 if st.button('OK'): 318 st.rerun()
Display the DZ coordinates in a dialog if one is selected in the drop zones
selection box. The selection is stored in the st.session_state.currentDropZone
variable. If a valid name is available, the latitude and longitude are
displayed. If None
or invalid, a notification dialog is displayed.
The corresponding UI button is only enabled if the user selected a valid DZ, otherwise the button is disabled.
321def setSideBarAndMain(icon: str, singleTrack: bool, selectDZState): 322 """ 323 Set all the interactive and navigational components for the app's side bar. 324 325 Arguments 326 --------- 327 icon 328 A meaningful Emoji associated with the the side bar's title. 329 330 singleTrack 331 A flag for allowing selection of a single or multiple track files in the 332 corresponding selector component. Determines whether the side bar is used 333 for the single- or multiple selection application. 334 335 selectDZState 336 A callback for the drop zone selector selection box, affected by events in 337 the main application. 338 339 Notes 340 ----- 341 All the aplication level values associated with the components and 342 selections from the side bar are stored in `st.session_state` and visible 343 across the whole application. 344 345 **Do not** cache calls to `ssscoring.appcommon.setSideBarAndMain()` because 346 this can result in unpredictable behavior since the cache may never be 347 cleared until application reload. 348 """ 349 dropZones = initDropZonesFromResource() 350 st.session_state.currentDropZone = None 351 elevation = None 352 st.sidebar.title('%s SSScore %s' % (icon, __VERSION__)) 353 st.session_state.processBadJump = st.sidebar.checkbox('Process bad jumps', value=True, help='Display results from invalid jumps') 354 st.session_state.currentDropZone = st.sidebar.selectbox('Select the drop zone:', dropZones.dropZone, index=None, on_change=selectDZState, disabled=(elevation != None and elevation != 0.0)) 355 elevation = st.sidebar.number_input('...or enter the DZ elevation in meters:', min_value=0.0, max_value=4000.0, value='min', format='%.2f', disabled=(st.session_state.currentDropZone != None), on_change=selectDZState) 356 if st.session_state.currentDropZone: 357 st.session_state.elevation = dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].elevation 358 elif elevation != None and elevation != 0.0: 359 st.session_state.elevation= elevation 360 else: 361 st.session_state.elevation = None 362 st.session_state.trackFiles = None 363 st.sidebar.metric('Elevation', value='%.1f m' % (0.0 if st.session_state.elevation == None else st.session_state.elevation)) 364 if singleTrack: 365 trackFile = st.sidebar.file_uploader('Track file', type=[ 'csv', ], disabled=st.session_state.elevation == None, key = st.session_state.uploaderKey) 366 if trackFile: 367 st.session_state.trackFile = trackFile 368 else: 369 trackFiles = st.sidebar.file_uploader( 370 'Track files', 371 type=[ 'csv', ], 372 disabled=st.session_state.elevation == None, 373 accept_multiple_files=True, 374 key = st.session_state.uploaderKey 375 ) 376 if trackFiles: 377 st.session_state.trackFiles = trackFiles 378 st.sidebar.button('Clear', on_click=selectDZState) 379 st.sidebar.button('Display DZ coordinates', on_click=displayDZCoordinates, disabled=(st.session_state.currentDropZone == None)) 380 st.sidebar.link_button('Report missing DZ', 'https://github.com/pr3d4t0r/SSScoring/issues/new?template=report-missing-dz.md', icon=':material/breaking_news_alt_1:') 381 st.sidebar.link_button('Feature request or bug report', 'https://github.com/pr3d4t0r/SSScoring/issues/new?template=Blank+issue', icon=':material/breaking_news_alt_1:')
Set all the interactive and navigational components for the app's side bar.
Arguments
icon
A meaningful Emoji associated with the the side bar's title.
singleTrack
A flag for allowing selection of a single or multiple track files in the corresponding selector component. Determines whether the side bar is used for the single- or multiple selection application.
selectDZState
A callback for the drop zone selector selection box, affected by events in the main application.
Notes
All the aplication level values associated with the components and
selections from the side bar are stored in st.session_state
and visible
across the whole application.
Do not cache calls to ssscoring.appcommon.setSideBarAndMain()
because
this can result in unpredictable behavior since the cache may never be
cleared until application reload.
384def setSideBarDeprecated(icon: str): 385 """ 386 Set a disabled version of the sidebar to maintain UX compatibility with the 387 actual app, used when the main screen is configured to display an end-user 388 message like "we moved to a new domain." 389 390 Arguments 391 --------- 392 icon 393 A meaningful Emoji associated with the the side bar's title. 394 """ 395 dropZones = initDropZonesFromResource() 396 st.session_state.currentDropZone = None 397 elevation = None 398 st.sidebar.title('%s SSScore %s' % (icon, __VERSION__)) 399 st.session_state.processBadJump = st.sidebar.checkbox('Process bad jumps', value=True, help='Display results from invalid jumps', disabled=True) 400 st.session_state.currentDropZone = st.sidebar.selectbox('Select the drop zone:', dropZones.dropZone, index=None, disabled=True) 401 elevation = st.sidebar.number_input('...or enter the DZ elevation in meters:', min_value=0.0, max_value=4000.0, value='min', format='%.2f', disabled=True) 402 if st.session_state.currentDropZone: 403 st.session_state.elevation = dropZones[dropZones.dropZone == st.session_state.currentDropZone ].iloc[0].elevation 404 elif elevation != None and elevation != 0.0: 405 st.session_state.elevation= elevation 406 else: 407 st.session_state.elevation = None 408 st.session_state.trackFiles = None 409 st.sidebar.metric('Elevation', value='%.1f m' % (0.0 if st.session_state.elevation == None else st.session_state.elevation)) 410 trackFile = st.sidebar.file_uploader('Track file', type=[ 'csv', ], disabled=True) 411 st.sidebar.button('Clear', disabled=True) 412 st.sidebar.button('Display DZ coordinates', disabled=True)
Set a disabled version of the sidebar to maintain UX compatibility with the actual app, used when the main screen is configured to display an end-user message like "we moved to a new domain."
Arguments
icon
A meaningful Emoji associated with the the side bar's title.