diff --git a/cara/apps/calculator/README.md b/cara/apps/calculator/README.md index 8d9685af..acb57c3c 100644 --- a/cara/apps/calculator/README.md +++ b/cara/apps/calculator/README.md @@ -61,10 +61,11 @@ Please enter the number, height and width and opening distance of the windows (i If there are multiple windows of different sizes, you should take an average. The window opening distance (in m) is: -* In the case of Sliding or Side-Hung option, the length the window is moved open -* In case of Top- or Bottom-Hung, the distance between the fixed frame and the movable glazed part when open - Window opening distance example (image of open window and measuring tape): +* In the case of Sliding or Side-Hung option, the length the window is moved open. + _Window opening distance example (image of open window and measuring tape):_ ![Window Opening Distance](static/images/window_opening.png "How to measure window opening distance") +* In case of Top- or Bottom-Hung, the distance between the fixed frame and the movable glazed part when open. + **Notes**: If you are unsure about the opening distance for the window, it is recommended to choose a conservative value (5 cms, 0.05m or 10cms, 0.10m). @@ -72,7 +73,7 @@ If you open the window at different distances throughout the day, choose an aver When using natural ventilation, the circulation of air is simulated as a function of the difference between the temperature inside the room and the outside air temperature. The average outdoor temperature for each hour of the day has been computed for every month of the year based on historical data for Geneva, Switzerland. It is therefore very important to enter the correct time and date in the event data section. -Finally, you must specify when the windows are open - all the time (always), or for 10 minutes every 2 hours. +Finally, you must specify if the windows are open permanently (at all the times), or periodically (in intervals for a certain duration and frequency - both in minutes) - e.g. open the window for 10 minutes (duration) every 60 minutes (frequency). #### No ventilation This option assumes there is neither Mechanical nor Natural ventilation in the simulation. diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 1974e6df..8a2628f5 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -43,6 +43,8 @@ class FormData: total_people: int ventilation_type: str volume_type: str + windows_duration: float + windows_frequency: float window_height: float window_type: str window_width: float @@ -121,6 +123,8 @@ class FormData: total_people=int(form_data['total_people']), ventilation_type=form_data['ventilation_type'], volume_type=form_data['volume_type'], + windows_duration=float(form_data['windows_duration']), + windows_frequency=float(form_data['windows_frequency']), window_height=float(form_data['window_height']), window_type=form_data['window_type'], window_width=float(form_data['window_width']), @@ -138,7 +142,7 @@ class FormData: # Initializes a ventilation instance as a window if 'natural' is selected, or as a HEPA-filter otherwise if self.ventilation_type == 'natural': if self.windows_open == 'interval': - window_interval = models.PeriodicInterval(120, 10) + window_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration) else: window_interval = always_on @@ -424,7 +428,7 @@ def baseline_raw_form_data(): 'mask_type': 'Type I', 'mask_wearing': 'removed', 'mechanical_ventilation_type': '', - 'model_version': 'v1.1.0', + 'model_version': 'v1.2.0', 'opening_distance': '0.2', 'recurrent_event_month': 'January', 'room_number': '123', @@ -434,11 +438,13 @@ def baseline_raw_form_data(): 'total_people': '10', 'ventilation_type': 'natural', 'volume_type': 'room_volume', + 'windows_duration': '', + 'windows_frequency': '', 'window_height': '2', 'window_type': 'sliding', 'window_width': '2', 'windows_number': '1', - 'windows_open': 'interval' + 'windows_open': 'always' } @@ -449,7 +455,7 @@ MASK_TYPES = {'Type I', 'FFP2'} MASK_WEARING = {'continuous', 'removed'} VENTILATION_TYPES = {'natural', 'mechanical', 'no-ventilation'} VOLUME_TYPES = {'room_volume', 'room_dimensions'} -WINDOWS_OPEN = {'always', 'interval', 'breaks', 'not-applicable'} +WINDOWS_OPEN = {'always', 'interval', 'not-applicable'} WINDOWS_TYPES = {'sliding', 'hinged', 'not-applicable'} diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 278a0a79..27abbf80 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -113,6 +113,29 @@ def minutes_to_time(minutes: int) -> str: return f"{hour_string}:{minute_string}" +def readable_minutes(minutes: int) -> str: + + time = minutes + unit = " minute" + if time % 60 == 0: + time = minutes/60 + unit = " hour" + if time != 1: + unit += "s" + + if time.is_integer(): + time = "{:0.0f}".format(time) + else: + time = "{0:.2f}".format(time) + + return time + unit + +def non_zero_percentage (percentage: int) -> str: + + if percentage < 0.01: + return "<0.01%" + else: + return "{:0.0f}%".format(percentage) def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models.ExposureModel]: scenarios = {} @@ -223,6 +246,8 @@ def build_report(model: models.ExposureModel, form: FormData): loader=jinja2.FileSystemLoader([cara_templates, calculator_templates]), undefined=jinja2.StrictUndefined, ) + env.filters['non_zero_percentage'] = non_zero_percentage + env.filters['readable_minutes'] = readable_minutes env.filters['minutes_to_time'] = minutes_to_time env.filters['float_format'] = "{0:.2f}".format env.filters['int_format'] = "{:0.0f}".format diff --git a/cara/apps/calculator/static/css/form.css b/cara/apps/calculator/static/css/form.css index 24781792..294badd0 100644 --- a/cara/apps/calculator/static/css/form.css +++ b/cara/apps/calculator/static/css/form.css @@ -10,6 +10,10 @@ color: red; } +.tabbed { + padding-left: 15pt; +} + #disclaimer,#code_license { font-size: 9pt; } diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js index fef4d621..c4983578 100644 --- a/cara/apps/calculator/static/js/form.js +++ b/cara/apps/calculator/static/js/form.js @@ -1,7 +1,7 @@ /* -------HTML structure------- */ function getChildElement(elem) { // Get the element named in the given element's data-enables attribute. - return $("#" + elem.data("enables")); + return $(elem.data("enables")); } function insertErrorFor(referenceNode, text) { @@ -49,6 +49,12 @@ function require_fields(obj) { require_air_changes(false); require_air_supply(true); break; + case "interval": + require_venting(true); + break; + case "always": + require_venting(false); + break; case "hepa_yes": require_hepa(true); break; @@ -95,14 +101,14 @@ function unrequire_fields(obj) { function require_room_volume(option) { require_input_field("#room_volume", option); - disable_input_field("#room_volume", !option); + set_disabled_status("#room_volume", !option); } function require_room_dimensions(option) { require_input_field("#floor_area", option); require_input_field("#ceiling_height", option); - disable_input_field("#floor_area", !option); - disable_input_field("#ceiling_height", !option); + set_disabled_status("#floor_area", !option); + set_disabled_status("#ceiling_height", !option); } function require_mechanical_ventilation(option) { @@ -122,34 +128,38 @@ function require_natural_ventilation(option) { $("#window_hinged").prop('required', option); $("#always").prop('required', option); $("#interval").prop('required', option); - - $("#window_sliding").prop('checked', option); - require_window_width(false); } function require_window_width(option) { require_input_field("#window_width", option); - disable_input_field("#window_width", !option); + set_disabled_status("#window_width", !option); } function require_air_changes(option) { require_input_field("#air_changes", option); - disable_input_field("#air_changes", !option); + set_disabled_status("#air_changes", !option); } function require_air_supply(option) { require_input_field("#air_supply", option); - disable_input_field("#air_supply", !option); + set_disabled_status("#air_supply", !option); +} + +function require_venting(option) { + require_input_field("#windows_duration", option); + require_input_field("#windows_frequency", option); + set_disabled_status("#windows_duration", !option); + set_disabled_status("#windows_frequency", !option); } function require_single_event(option) { require_input_field("#single_event_date", option); - disable_input_field("#single_event_date", !option); + set_disabled_status("#single_event_date", !option); } function require_recurrent_event(option) { $("#recurrent_event_month").prop('required', option); - disable_input_field("#recurrent_event_month", !option); + set_disabled_status("#recurrent_event_month", !option); } function require_lunch(option) { @@ -182,7 +192,7 @@ function require_mask(option) { function require_hepa(option) { require_input_field("#hepa_amount", option); - disable_input_field("#hepa_amount", !option); + set_disabled_status("#hepa_amount", !option); } function require_input_field(id, option) { @@ -192,7 +202,7 @@ function require_input_field(id, option) { } } -function disable_input_field(id, option) { +function set_disabled_status(id, option) { if (option) $(id).addClass("disabled"); else @@ -228,10 +238,9 @@ function on_ventilation_type_change() { } else { getChildElement($(this)).hide(); unrequire_fields(this); - // Clear the inputs for this newly hidden child element. + + // Clear inputs for this newly hidden child element. getChildElement($(this)).find('input').not('input[type=radio]').val(''); - getChildElement($(this)).find('input[type=radio]').prop("checked", false); - getChildElement($(this)).find('input').prop("required", false); } }); } @@ -259,75 +268,28 @@ function show_disclaimer() { } } -$(".has_radio").on('click', function(event){ - click_radio(this.id); +$("[data-has-radio]").on('click', function(event){ + $($(this).data("has-radio")).click(); }); -$(".has_radio").on('change', function(event){ - click_radio(this.id); +$("[data-has-radio]").on('change', function(event){ + $($(this).data("has-radio")).click(); }); -function click_radio(id) { - switch (id) { - case "room_volume": - $("#room_type_volume").click(); - break; - case "floor_area": - case "ceiling_height": - $("#room_type_dimensions").click(); - break; - case "air_supply": - $("#air_type_supply").click(); - break; - case "air_changes": - $("#air_type_changes").click(); - break; - case "window_width": - $("#window_hinged").click(); - break; - case "hepa_amount": - $("#hepa_yes").click(); - break; - case "single_event_date": - $("#event_type_single").click(); - break; - case "recurrent_event_month": - $("#event_type_recurrent").click(); - break; - default: - break; - } -} - /* -------Form validation------- */ function validate_form(form) { var submit = true; - //Validate all non zero values - $("input[required].non_zero").each(function() { - if (!validateValue(this)) { + // Activity times and lunch break times are co-dependent + // -> So if 1 fails it doesn't make sense to check the rest + + //Validate all finish times + $("input[required].finish_time").each(function() { + if (!validateFinishTime(this)) { submit = false; } }); - //Validate all dates - if (submit) { - $("input[required].datepicker").each(function() { - if (!validateDate(this)) { - submit = false; - } - }); - } - - //Validate all times - if (submit) { - $("input[required].finish_time").each(function() { - if (!validateFinishTime(this)) { - submit = false; - } - }); - } - //Validate all lunch breaks if (submit) { $("input[required].lunch").each(function() { @@ -337,7 +299,7 @@ function validate_form(form) { }); } - //Check if breaks length >= activity length + //Validate breaks length < activity length if (submit) { var activityBreaksObj= document.getElementById("activity_breaks"); removeErrorFor(activityBreaksObj); @@ -363,6 +325,32 @@ function validate_form(form) { } } + //Validate all non zero values + $("input[required].non_zero").each(function() { + if (!validateValue(this)) { + submit = false; + } + }); + + //Validate all dates + $("input[required].datepicker").each(function() { + if (!validateDate(this)) { + submit = false; + } + }); + + //Validate window venting duration < venting frequency + if (!$("#windows_duration").hasClass("disabled")) { + var windowsDurationObj = document.getElementById("windows_duration"); + var windowsFrequencyObj = document.getElementById("windows_frequency"); + removeErrorFor(windowsFrequencyObj); + + if (parseInt(windowsDurationObj.value) >= parseInt(windowsFrequencyObj.value)) { + insertErrorFor(windowsFrequencyObj, "Duration >= Frequency"); + submit = false; + } + } + return submit; } @@ -487,12 +475,8 @@ $(document).ready(function () { // Call the function now to handle forward/back button presses in the browser. on_ventilation_type_change(); - //Same for other options - require_fields($("input[name='lunch_option']:checked")); - require_fields($("input[name='volume_type']:checked")); - require_fields($("input[name='mechanical_ventilation_type']:checked")); - require_fields($("input[name='window_type']:checked")); - require_fields($("input[name='hepa_option']:checked")); + //Check all radio buttons previously selected + $("input[type=radio]:checked").each(function() {require_fields(this)}); // Setup the maximum number of people at page load (to handle back/forward), // and update it when total people is changed. @@ -517,11 +501,6 @@ $(document).ready(function () { $(".start_time[data-lunch-for]").each(function() {validateLunchBreak($(this).data('time-group'))}); $("[data-lunch-for]").change(function() {validateLunchBreak($(this).data('time-group'))}); $("[data-lunch-break]").change(function() {validateLunchBreak($(this).data('lunch-break'))}); - - var radioValue = $("input[name='event_type']:checked"); - if (radioValue.val()) { - require_fields(radioValue.get(0)); - } }); /* -------Debugging------- */ diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2 index cee8be34..274cc63f 100644 --- a/cara/apps/calculator/templates/calculator.form.html.j2 +++ b/cara/apps/calculator/templates/calculator.form.html.j2 @@ -1,6 +1,6 @@ {% extends "layout.html.j2" %} -{% set MODEL_VERSION="v1.1.0" %} +{% set MODEL_VERSION="v1.2.0" %} {% set DEBUG=False %} {% set active_page="calculator/" %} @@ -46,12 +46,12 @@
   -
+
   -
+
         -
+

@@ -62,21 +62,21 @@ Ventilation type:    - +    - +
-