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 = './data'

Default data lake directory when reading files from the local file system.

STREAMLIT_SIG_KEY = 'HOSTNAME'

Environment key used by the Streamlig.app environment when running an application.

STREAMLIT_SIG_VALUE = 'streamlit'

Expected value associate with the environment variable STREAMLIT_SIG_KEY when running in a Streamlit.app environment.

def isStreamlitHostedApp() -> bool:
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.

@st.cache_data
def fetchResource(resourceName: str) -> _io.StringIO:
 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.

def initDropZonesFromResource() -> pandas.core.frame.DataFrame:
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.

def displayJumpDataIn(resultsTable: pandas.core.frame.DataFrame):
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

ssscoring.datatypes.JumpResults

def interpretJumpResult( tag: str, jumpResult: ssscoring.datatypes.JumpResults, processBadJump: bool):
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 form
  • scoringInfo - Max speed, scoring window, etc.
  • badJumpLegend - A warning or error if the jump is invalid according to ISC rules
  • jumpStatus - An instance of JumpStatus that may have been overriden to OK if processBadJump was set to True and the jump was invalid. Used only for display. Use jumpResult.status to determine the actual result of the jump from a strict scoring perspective. jumpStatus is used for display override purposes only.
def plotJumpResult(tag: str, jumpResult: ssscoring.datatypes.JumpResults):
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.

def initFileUploaderState(filesObject: str, uploaderKey: str = 'uploaderKey'):
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.

def displayTrackOnMap(deck: pydeck.bindings.deck.Deck, displayScore=True):
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.

@st.dialog('DZ Coordinates')
def displayDZCoordinates():
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.

def setSideBarAndMain(icon: str, singleTrack: bool, selectDZState):
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.

def setSideBarDeprecated(icon: str):
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.