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