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

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

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

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

ssscoring.datatypes.JumpResults

def interpretJumpResult( tag: str, jumpResult: ssscoring.datatypes.JumpResults, processBadJump: bool):
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 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):
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.

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

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

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

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

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