diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 5a746648..d2019659 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -33,6 +33,7 @@ from . import markdown_tools from . import model_generator, co2_model_generator from .report_generator import ReportGenerator, calculate_report_data +from .co2_report_generator import CO2ReportGenerator from .user import AuthenticatedUser, AnonymousUser # The calculator version is based on a combination of the model version and the @@ -404,7 +405,10 @@ async def post(self, endpoint: str) -> None: requested_model_config = tornado.escape.json_decode(self.request.body) try: - form = co2_model_generator.CO2FormData.from_dict(requested_model_config, data_registry) + form: co2_model_generator.CO2FormData = co2_model_generator.CO2FormData.from_dict( + requested_model_config, + data_registry + ) except Exception as err: if self.settings.get("debug", False): import traceback @@ -414,29 +418,21 @@ async def post(self, endpoint: str) -> None: self.finish(json.dumps(response_json)) return + CO2_report_generator: CO2ReportGenerator = CO2ReportGenerator() if endpoint.rstrip('/') == 'plot': - transition_times = co2_model_generator.CO2FormData.find_change_points_with_pelt(form.CO2_data) - self.finish({'CO2_plot': co2_model_generator.CO2FormData.generate_ventilation_plot(form.CO2_data, transition_times), - 'transition_times': [round(el, 2) for el in transition_times]}) + report = CO2_report_generator.build_initial_plot(form) + self.finish(report) else: executor = loky.get_reusable_executor( max_workers=self.settings['handler_worker_pool_size'], timeout=300, ) report_task = executor.submit( - co2_model_generator.CO2FormData.build_model, form, + CO2_report_generator.build_fitting_results, form, ) + report = await asyncio.wrap_future(report_task) - - result = dict(report.CO2_fit_params()) - ventilation_transition_times = report.ventilation_transition_times - - result['fitting_ventilation_type'] = form.fitting_ventilation_type - result['transition_times'] = ventilation_transition_times - result['CO2_plot'] = co2_model_generator.CO2FormData.generate_ventilation_plot(CO2_data=form.CO2_data, - transition_times=ventilation_transition_times[:-1], - predictive_CO2=result['predictive_CO2']) - self.finish(result) + self.finish(report) def get_url(app_root: str, relative_path: str = '/'): diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index e8a054ca..f9acdf61 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -2,8 +2,9 @@ import logging import typing import numpy as np -import ruptures as rpt import matplotlib.pyplot as plt +from scipy.signal import find_peaks +import pandas as pd import re from caimira import models @@ -21,13 +22,13 @@ class CO2FormData(FormData): CO2_data: dict fitting_ventilation_states: list - fitting_ventilation_type: str room_capacity: typing.Optional[int] #: The default values for undefined fields. Note that the defaults here #: and the defaults in the html form must not be contradictory. _DEFAULTS: typing.ClassVar[typing.Dict[str, typing.Any]] = { 'CO2_data': '{}', + 'fitting_ventilation_states': '[]', 'exposed_coffee_break_option': 'coffee_break_0', 'exposed_coffee_duration': 5, 'exposed_finish': '17:30', @@ -35,8 +36,6 @@ class CO2FormData(FormData): 'exposed_lunch_option': True, 'exposed_lunch_start': '12:30', 'exposed_start': '08:30', - 'fitting_ventilation_states': '[]', - 'fitting_ventilation_type': 'fitting_natural_ventilation', 'infected_coffee_break_option': 'coffee_break_0', 'infected_coffee_duration': 5, 'infected_dont_have_breaks_with_exposed': False, @@ -97,55 +96,75 @@ def validate(self): raise TypeError(f'Unable to fetch "finish_time" key. Got "{dict_keys[1]}".') for time in input_break.values(): if not re.compile("^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$").match(time): - raise TypeError(f'Wrong time format - "HH:MM". Got "{time}".') + raise TypeError(f'Wrong time format - "HH:MM". Got "{time}".') - @classmethod - def find_change_points_with_pelt(self, CO2_data: dict): + def find_change_points(self) -> list: """ - Perform change point detection using Pelt algorithm from ruptures library with pen=15. - Returns a list of tuples containing (index, X-axis value) for the detected significant changes. + Perform change point detection using scipy library (find_peaks method) with rolling average of data. + Incorporate existing state change candidates and adjust the result accordingly. + Returns a list of the detected ventilation state changes, discarding any occupancy state change. """ - - times: list = CO2_data['times'] - CO2_values: list = CO2_data['CO2'] + times: list = self.CO2_data['times'] + CO2_values: list = self.CO2_data['CO2'] if len(times) != len(CO2_values): raise ValueError("times and CO2 values must have the same length.") - # Convert the input list to a numpy array for use with the ruptures library - CO2_np = np.array(CO2_values) + # Time difference between two consecutive time data entries, in seconds + diff = (times[1] - times[0]) * 3600 # Initial data points in absolute hours, e.g. 14.78 + + # Calculate minimum interval for smoothing technique + smooth_min_interval_in_minutes = 1 # Minimum time difference for smooth technique + window_size = max(int((smooth_min_interval_in_minutes * 60) // diff), 1) - # Define the model for change point detection (Radial Basis Function kernel) - model = "rbf" + # Applying a rolling average to smooth the initial data + smoothed_co2 = pd.Series(CO2_values).rolling(window=window_size, center=True).mean() - # Fit the Pelt algorithm to the data with the specified model - algo = rpt.Pelt(model=model).fit(CO2_np) + # Calculate minimum interval for peaks and valleys detection + peak_valley_min_interval_in_minutes = 15 # Minimum time difference between two peaks or two valleys + min_distance_points = max(int((peak_valley_min_interval_in_minutes * 60) // diff), 1) - # Predict change points using the Pelt algorithm with a penalty value of 15 - result = algo.predict(pen=15) + # Calculate minimum width of datapoints for valley detection + width_min_interval_in_minutes = 20 # Minimum duration of a valley + min_valley_width = max(int((width_min_interval_in_minutes * 60) // diff), 1) - # Find local minima and maxima - segments = np.split(np.arange(len(CO2_values)), result) - merged_segments = [np.hstack((segments[i], segments[i + 1])) for i in range(len(segments) - 1)] - result_set = set() - for segment in merged_segments[:-2]: - result_set.add(times[CO2_values.index(min(CO2_np[segment]))]) - result_set.add(times[CO2_values.index(max(CO2_np[segment]))]) - return list(result_set) + # Find peaks (maxima) in the smoothed data applying the distance factor + peaks, _ = find_peaks(smoothed_co2.values, prominence=100, distance=min_distance_points) + + # Find valleys (minima) by inverting the smoothed data and applying the width and distance factors + valleys, _ = find_peaks(-smoothed_co2.values, prominence=50, width=min_valley_width, distance=min_distance_points) - @classmethod - def generate_ventilation_plot(self, CO2_data: dict, - transition_times: typing.Optional[list] = None, - predictive_CO2: typing.Optional[list] = None): - times_values = CO2_data['times'] - CO2_values = CO2_data['CO2'] + # Extract peak and valley timestamps + timestamps = np.array(times) + peak_timestamps = timestamps[peaks] + valley_timestamps = timestamps[valleys] + + return sorted(np.concatenate((peak_timestamps, valley_timestamps))) + + def generate_ventilation_plot(self, + ventilation_transition_times: typing.Optional[list] = None, + occupancy_transition_times: typing.Optional[list] = None, + predictive_CO2: typing.Optional[list] = None) -> str: + + # Plot data (x-axis: times; y-axis: CO2 concentrations) + times_values: list = self.CO2_data['times'] + CO2_values: list = self.CO2_data['CO2'] fig = plt.figure(figsize=(7, 4), dpi=110) plt.plot(times_values, CO2_values, label='Input CO₂') - if (transition_times): - for time in transition_times: - plt.axvline(x = time, color = 'grey', linewidth=0.5, linestyle='--') + # Add occupancy state changes: + if (occupancy_transition_times): + for i, time in enumerate(occupancy_transition_times): + plt.axvline(x = time, color = 'grey', linewidth=0.5, linestyle='--', label='Occupancy change (from input)' if i == 0 else None) + # Add ventilation state changes: + if (ventilation_transition_times): + for i, time in enumerate(ventilation_transition_times): + if i == 0: + label = 'Ventilation change (detected)' if occupancy_transition_times else 'Ventilation state changes' + else: label = None + plt.axvline(x = time, color = 'red', linewidth=0.5, linestyle='--', label=label) + if (predictive_CO2): plt.plot(times_values, predictive_CO2, label='Predictive CO₂') plt.xlabel('Time of day') @@ -158,14 +177,18 @@ def population_present_changes(self, infected_presence: models.Interval, exposed state_change_times.update(exposed_presence.transition_times()) return sorted(state_change_times) - def ventilation_transition_times(self) -> typing.Tuple[float, ...]: - # Check what type of ventilation is considered for the fitting - if self.fitting_ventilation_type == 'fitting_natural_ventilation': - vent_states = self.fitting_ventilation_states - vent_states.append(self.CO2_data['times'][-1]) - return tuple(vent_states) - else: - return tuple((self.CO2_data['times'][0], self.CO2_data['times'][-1])) + def ventilation_transition_times(self) -> typing.Tuple[float]: + ''' + Check if the last time from the input data is + included in the ventilation ventilations state. + Given that the last time is a required state change, + if not included, this method adds it. + ''' + vent_states = self.fitting_ventilation_states + last_time_from_input = self.CO2_data['times'][-1] + if (vent_states and last_time_from_input != vent_states[-1]): # The last time value is always needed for the last ACH interval. + vent_states.append(last_time_from_input) + return tuple(vent_states) def build_model(self, size=None) -> models.CO2DataModel: # type: ignore size = size or self.data_registry.monte_carlo['sample_size'] @@ -184,7 +207,7 @@ def build_model(self, size=None) -> models.CO2DataModel: # type: ignore activity=None, # type: ignore ) - all_state_changes=self.population_present_changes(infected_presence, exposed_presence) + all_state_changes = self.population_present_changes(infected_presence, exposed_presence) total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop) for _, stop in zip(all_state_changes[:-1], all_state_changes[1:])] diff --git a/caimira/apps/calculator/co2_report_generator.py b/caimira/apps/calculator/co2_report_generator.py new file mode 100644 index 00000000..caf67148 --- /dev/null +++ b/caimira/apps/calculator/co2_report_generator.py @@ -0,0 +1,68 @@ +import dataclasses +import typing + +from caimira.models import CO2DataModel, Interval, IntPiecewiseConstant +from .co2_model_generator import CO2FormData + + +@dataclasses.dataclass +class CO2ReportGenerator: + + def build_initial_plot( + self, + form: CO2FormData, + ) -> dict: + ''' + Initial plot with the suggested ventilation state changes. + This method receives the form input and returns the CO2 + plot with the respective transition times. + ''' + CO2model: CO2DataModel = form.build_model() + + occupancy_transition_times = list(CO2model.occupancy.transition_times) + + ventilation_transition_times: list = form.find_change_points() + # The entire ventilation changes consider the initial and final occupancy state change + all_vent_transition_times: list = sorted( + [occupancy_transition_times[0]] + + [occupancy_transition_times[-1]] + + ventilation_transition_times) + + ventilation_plot: str = form.generate_ventilation_plot( + ventilation_transition_times=all_vent_transition_times, + occupancy_transition_times=occupancy_transition_times + ) + + context = { + 'CO2_plot': ventilation_plot, + 'transition_times': [round(el, 2) for el in all_vent_transition_times], + } + + return context + + def build_fitting_results( + self, + form: CO2FormData, + ) -> dict: + ''' + Final fitting results with the respective predictive CO2. + This method receives the form input and returns the fitting results + along with the CO2 plot with the predictive CO2. + ''' + CO2model: CO2DataModel = form.build_model() + + # Ventilation times after user manipulation from the suggested ventilation state change times. + ventilation_transition_times = list(CO2model.ventilation_transition_times) + + # The result of the following method is a dict with the results of the fitting + # algorithm, namely the breathing rate and ACH values. It also returns the + # predictive CO2 result based on the fitting results. + context: typing.Dict = dict(CO2model.CO2_fit_params()) + + # Add the transition times and CO2 plot to the results. + context['transition_times'] = ventilation_transition_times + context['CO2_plot'] = form.generate_ventilation_plot(ventilation_transition_times=ventilation_transition_times[:-1], + predictive_CO2=context['predictive_CO2']) + + return context + \ No newline at end of file diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index d1895848..b0656851 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -330,13 +330,10 @@ def ventilation(self) -> models._VentilationBase: min(self.infected_start, self.exposed_start)/60) if self.ventilation_type == 'from_fitting': ventilations = [] - if self.CO2_fitting_result['fitting_ventilation_type'] == 'fitting_natural_ventilation': - transition_times = self.CO2_fitting_result['transition_times'] - for index, (start, stop) in enumerate(zip(transition_times[:-1], transition_times[1:])): - ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), - air_exch=self.CO2_fitting_result['ventilation_values'][index])) - else: - ventilations.append(models.AirChange(active=always_on, air_exch=self.CO2_fitting_result['ventilation_values'][0])) + transition_times = self.CO2_fitting_result['transition_times'] + for index, (start, stop) in enumerate(zip(transition_times[:-1], transition_times[1:])): + ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), + air_exch=self.CO2_fitting_result['ventilation_values'][index])) return models.MultipleVentilation(tuple(ventilations)) # Initializes a ventilation instance as a window if 'natural_ventilation' is selected, or as a HEPA-filter otherwise diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index dc83ccef..87155e96 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -368,9 +368,9 @@ def readable_minutes(minutes: int) -> str: def hour_format(hour: float) -> str: # Convert float hour to HH:MM format - hours = int(hour) - minutes = int(hour % 1 * 60) - return f"{hours}:{minutes if minutes != 0 else '00'}" + hours = f"{int(hour):02}" + minutes = f"{int(hour % 1 * 60):02}" + return f"{hours}:{minutes}" def percentage(absolute: float) -> float: diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 77b0b756..16df7060 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -9,7 +9,6 @@ const CO2_data_form = [ "exposed_lunch_start", "exposed_start", "fitting_ventilation_states", - "fitting_ventilation_type", "infected_coffee_break_option", "infected_coffee_duration", "infected_dont_have_breaks_with_exposed", @@ -25,7 +24,7 @@ const CO2_data_form = [ "total_people", ]; -// Method to upload a valid excel file +// Method to upload a valid data file (accepted formats: .xls and .xlsx) function uploadFile(endpoint) { clearFittingResultComponent(); const files = $("#file_upload")[0].files; @@ -41,12 +40,12 @@ function uploadFile(endpoint) { .toUpperCase(); if (extension !== ".XLS" && extension !== ".XLSX") { $("#upload-error") - .text("Please select a valid excel file (.XLS or .XLSX).") + .text("Please select a valid data file (.XLS or .XLSX).") .show(); return; } - // FileReader API to read the Excel file + // FileReader API to read the data file const reader = new FileReader(); reader.onload = function (event) { const fileContent = event.target.result; @@ -80,7 +79,7 @@ function uploadFile(endpoint) { if (data.length <= 1) { $("#upload-error") .text( - "The Excel file is empty. Please make sure it contains data below the header row." + "The data file is empty. Please make sure it contains data below the header row." ) .show(); return; @@ -107,7 +106,40 @@ function uploadFile(endpoint) { } } - // Convert Excel file to JSON and further processing + // Validate times data encompass simulation time + const firstTimeInData = parseFloat((data[1][timesColumnIndex] * 60).toFixed(2)) + const lastTimeInData = parseFloat((data[data.length - 1][timesColumnIndex] * 60).toFixed(2)) + // Validate start time + const infected_start = $(`[name=infected_start]`).first().val(); + const exposed_start = $(`[name=exposed_start]`).first().val(); + + let [hours_infected, minutes_infected] = infected_start.split(":").map(Number); + let elapsed_time_infected = hours_infected * 60 + minutes_infected; + + let [hours_exposed, minutes_exposed] = exposed_start.split(":").map(Number); + let elapsed_time_exposed = hours_exposed * 60 + minutes_exposed; + + const min_presence_time = parseFloat((Math.min(elapsed_time_infected, elapsed_time_exposed)).toFixed(2)); + + // Validate finish time + const infected_finish = $(`[name=infected_finish]`).first().val(); + const exposed_finish = $(`[name=exposed_finish]`).first().val(); + + [hours_infected, minutes_infected] = infected_finish.split(":").map(Number); + elapsed_time_infected = hours_infected * 60 + minutes_infected; + + [hours_exposed, minutes_exposed] = exposed_finish.split(":").map(Number); + elapsed_time_exposed = hours_exposed * 60 + minutes_exposed; + + const max_presence_time = parseFloat((Math.max(elapsed_time_infected, elapsed_time_exposed)).toFixed(2)); + if (firstTimeInData > min_presence_time || lastTimeInData < max_presence_time) { + $("#upload-error") + .text(`The times of the data file should encompass the entire simulation time (from ${min_presence_time/60} to ${max_presence_time/60}). + Got times from ${firstTimeInData/60} to ${lastTimeInData/60}. Either adapt the simulation presence times, or the times of the data file.`).show(); + return; + } + + // Convert data file to JSON and further processing try { generateJSONStructure(endpoint, data); // If all validations pass, process the file here or display a success message @@ -137,7 +169,6 @@ function generateJSONStructure(endpoint, jsonData) { inputToPopulate.val(JSON.stringify(finalStructure)); $("#generate_fitting_data").prop("disabled", false); $("#fitting_ventilation_states").prop("disabled", false); - $("[name=fitting_ventilation_type]").prop("disabled", false); $("#room_capacity").prop("disabled", false); plotCO2Data(endpoint); } @@ -177,66 +208,54 @@ function validateCO2Form() { if (validateFormInputs($("#button_fit_data"))) submit = true; const $fittingToSubmit = $('#DIVCO2_fitting_to_submit'); - // Check if natural ventilation is selected - if ( - $fittingToSubmit.find('input[name="fitting_ventilation_type"]:checked').val() == - "fitting_natural_ventilation" - ) { - // Validate ventilation scheme - const $ventilationStates = $fittingToSubmit.find("input[name=fitting_ventilation_states]"); - const $referenceNode = $("#DIVCO2_fitting_result"); - if ($ventilationStates.val() !== "") { - // validate input format - try { - const parsedValue = JSON.parse($ventilationStates.val()); - if (Array.isArray(parsedValue)) { - if (parsedValue.length <= 1) { + // Validate ventilation scheme + const $ventilationStates = $fittingToSubmit.find("input[name=fitting_ventilation_states]"); + const $referenceNode = $("#DIVCO2_fitting_result"); + if ($ventilationStates.val() !== "") { + // validate input format + try { + const parsedValue = JSON.parse($ventilationStates.val()); + if (Array.isArray(parsedValue)) { + if (parsedValue.length <= 1) { + insertErrorFor( + $referenceNode, + `'${$ventilationStates.attr('name')}' must have more than one ventilation state change (at least the beggining and end of simulation time).
` + ); + submit = false; + } + else { + const infected_finish = $(`[name=infected_finish]`).first().val(); + const exposed_finish = $(`[name=exposed_finish]`).first().val(); + + const [hours_infected, minutes_infected] = infected_finish.split(":").map(Number); + const elapsed_time_infected = hours_infected * 60 + minutes_infected; + + const [hours_exposed, minutes_exposed] = exposed_finish.split(":").map(Number); + const elapsed_time_exposed = hours_exposed * 60 + minutes_exposed; + + const max_presence_time = Math.max(elapsed_time_infected, elapsed_time_exposed); + const max_transition_time = parsedValue[parsedValue.length - 1] * 60; + + if (max_transition_time > max_presence_time) { insertErrorFor( $referenceNode, - `'${$ventilationStates.attr('name')}' must have more than one $ventilationStates.
` + `The last transition time (${parsedValue[parsedValue.length - 1]}) should be before the last presence time (${max_presence_time / 60}).
` ); submit = false; } - else { - const infected_finish = $(`[name=infected_finish]`).first().val(); - const exposed_finish = $(`[name=exposed_finish]`).first().val(); - - const [hours_infected, minutes_infected] = infected_finish.split(":").map(Number); - const elapsed_time_infected = hours_infected * 60 + minutes_infected; - - const [hours_exposed, minutes_exposed] = exposed_finish.split(":").map(Number); - const elapsed_time_exposed = hours_exposed * 60 + minutes_exposed; - - const max_presence_time = Math.max(elapsed_time_infected, elapsed_time_exposed); - const max_transition_time = parsedValue[parsedValue.length - 1] * 60; - - if (max_transition_time > max_presence_time) { - insertErrorFor( - $referenceNode, - `The last transition time (${parsedValue[parsedValue.length - 1]}) should be before the last presence time (${max_presence_time / 60}).
` - ); - submit = false; - } - } } - else { - insertErrorFor( - $referenceNode, - `'${$ventilationStates.attr('name')}' must be a list.
` - ); - submit = false; - } - } catch { + } + else { insertErrorFor( $referenceNode, - `'${$ventilationStates.attr('name')}' must be a list of numbers.
` + `'${$ventilationStates.attr('name')}' must be a list.
` ); submit = false; } - } else { + } catch { insertErrorFor( $referenceNode, - `'${$ventilationStates.attr('name')}' must be defined.
` + `'${$ventilationStates.attr('name')}' must be a list of numbers.
` ); submit = false; } @@ -253,23 +272,24 @@ function validateCO2Form() { submit = false; } } + } else { + insertErrorFor( + $referenceNode, + `'${$ventilationStates.attr('name')}' must be defined.
` + ); + submit = false; } - return submit; } function displayTransitionTimesHourFormat(start, stop) { - var minutes_start = ((start % 1) * 60).toPrecision(2); - var minutes_stop = ((stop % 1) * 60).toPrecision(2); - return ( - Math.floor(start) + - ":" + - (minutes_start != "0.0" ? minutes_start : "00") + - " - " + - Math.floor(stop) + - ":" + - (minutes_stop != "0.0" ? minutes_stop : "00") - ); + const formatTime = (time) => { + const hours = Math.floor(time); + const minutes = Math.round((time % 1) * 60); + return `${hours}:${minutes.toString().padStart(2, '0')}`; + }; + + return `${formatTime(start)} - ${formatTime(stop)}`; } function displayFittingData(json_response) { @@ -317,7 +337,7 @@ function displayFittingData(json_response) { $("#disable_fitting_algorithm").prop("disabled", false); $("#ventilation_rate_fit").html(ventilation_table); - $("#generate_fitting_data").html("Fit data"); + $("#generate_fitting_data").html("Confirm and Fit data"); $("#generate_fitting_data").hide(); $("#save_and_dismiss_dialog").show(); } @@ -379,7 +399,6 @@ function submitFittingAlgorithm(url) { "disabled", true ); - // Prepare data for submission const CO2_mapping = formatCO2DataForm(CO2_data_form); $("#CO2_input_data_div").show(); @@ -423,12 +442,6 @@ function clearFittingResultComponent() { $referenceNode.find("#DIVCO2_fitting_to_submit").hide(); $referenceNode.find("#CO2_data_plot").attr("src", ""); - // Update the ventilation scheme components - $referenceNode.find("#fitting_ventilation_states, [name=fitting_ventilation_type]").prop( - "disabled", - false - ); - // Update the bottom right buttons $referenceNode.find("#generate_fitting_data").show(); $referenceNode.find("#save_and_dismiss_dialog").hide(); diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js index e63bbb45..631be3cc 100644 --- a/caimira/apps/calculator/static/js/form.js +++ b/caimira/apps/calculator/static/js/form.js @@ -479,20 +479,6 @@ function on_coffee_break_option_change() { } } -function on_CO2_fitting_ventilation_change() { - ventilation_options = $('input[type=radio][name=fitting_ventilation_type]'); - ventilation_options.each(function (index) { - if (this.checked) { - getChildElement($(this)).show(); - require_fields(this); - } - else { - getChildElement($(this)).hide(); - require_fields(this); - } - }) -} - /* -------UI------- */ function show_disclaimer() { @@ -1070,12 +1056,6 @@ $(document).ready(function () { // Call the function now to handle forward/back button presses in the browser. on_coffee_break_option_change(); - // When the ventilation on the fitting changes we want to make its respective - // children show/hide. - $("input[type=radio][name=fitting_ventilation_type]").change(on_CO2_fitting_ventilation_change); - // Call the function now to handle forward/back button presses in the browser. - on_CO2_fitting_ventilation_change(); - // Setup the maximum number of people at page load (to handle back/forward), // and update it when total people is changed. validateMaxInfectedPeople(); diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index fa97661e..cabc5adf 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -323,7 +323,7 @@
- +
- +