diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..fb610bd63 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,29 @@ +# .coveragerc to control coverage.py +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + except ImportError + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + +ignore_errors = True +# fail_under = 70 +show_missing = True + +omit = + **/*_test.py + env/**/*.py + venv/**/*.py + ENV/**/*.py + VENV/**/*.py + desktop/env/**/*.py + desktop/venv/**/*.py + desktop/ENV/**/*.py + desktop/VENV/**/*.py + browser/env/**/*.py + browser/venv/**/*.py + browser/ENV/**/*.py + browser/VENV/**/*.py + browser/conftest.py diff --git a/.github/workflows/browser-tests.yaml b/.github/workflows/browser-tests.yaml new file mode 100644 index 000000000..911dc29e4 --- /dev/null +++ b/.github/workflows/browser-tests.yaml @@ -0,0 +1,36 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: browser + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r browser/requirements.txt + pip install -r browser/requirements-test.txt + + - name: Test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pytest browser --cov browser --pep8 + coveralls diff --git a/.gitignore b/.gitignore index c023183de..9428aa02e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,14 @@ __pycache__/ # C extensions *.so +# OS generated files .DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db # Distribution / packaging .Python diff --git a/README.md b/README.md index 55e9dcdb9..7d25cef85 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Caliban: Data Curation Tools for DeepCell. +[![Actions Status](https://github.com/vanvalenlab/caliban/workflows/browser/badge.svg)](https://github.com/vanvalenlab/caliban/actions) Caliban is a segmentation and tracking tool used for human-in-the-loop data curation. It displays lineage data along with raw and annotated images. The output files prepare this information as training data for DeepCell. @@ -62,7 +63,7 @@ You can use *esc* to return back to a state where no labels are selected. Keybinds in pixel editing mode are different from those in the label-editing mode. -Annotation mode focuses on using an adjustable brush to modify annotations on a pixel level, rather than using operations that apply to every instance of a label within a frame or set of frames. The brush tool will only make changes to the currently selected value. Ie, a brush set to edit label 5 will only add or erase "5" to the annotated image. +Annotation mode focuses on using an adjustable brush to modify annotations on a pixel level, rather than using operations that apply to every instance of a label within a frame or set of frames. The brush tool will only make changes to the currently selected value. Ie, a brush set to edit label 5 will only add or erase "5" to the annotated image. *[ (left bracket) / ] (right bracket)* - decrement/increment value that brush is applying @@ -76,7 +77,7 @@ Annotation mode focuses on using an adjustable brush to modify annotations on a *r* - turn on "conversion brush" setting, which changes brush behavior so that one label value is overwritten with another label value. No other labels are affected, and conversion brush will not draw on background. After turning on conversion brush, click on cell labels as prompted to set brush values. -*t* - threshold to predict annotations based on brightness. After turning this on, click and drag to draw a bounding box around the cell you wish to threshold. Make sure to include some background in the bounding box for accurate threshold predictions. Whatever was thresholded as foreground within the bounding box will be added to the annotation as a new cell with unique label. +*t* - threshold to predict annotations based on brightness. After turning this on, click and drag to draw a bounding box around the cell you wish to threshold. Make sure to include some background in the bounding box for accurate threshold predictions. Whatever was thresholded as foreground within the bounding box will be added to the annotation as a new cell with unique label. ### Viewing Options: @@ -90,7 +91,7 @@ Annotation mode focuses on using an adjustable brush to modify annotations on a *h* - switch between highlighted mode and non-highlighted mode (highlight exists in label- and pixel-editing modes but is displayed differently; label-editing highlighting recolors solid label with red, pixel-editing highlighting adds white or red outline around label in image). Once highlight mode is on, use *[ (left bracket) / ] (right bracket)* to decrement/increment selected cell label number. *shift+h* - switch between showing and hiding annotation masks in the pixel editor - + *z* - switch between annotations and raw images (outside of pixel editor) *i* - invert greyscale raw image (viewing raw image or in pixel editor) @@ -116,7 +117,7 @@ Annotation mode focuses on using an adjustable brush to modify annotations on a ### To Save: -Once done, use the following key to save the changed file. +Once done, use the following key to save the changed file. The tool will also save the original file in the same folder. In npz mode, a new npz file will be saved with a version number. An npz can be saved as a trk file (select "t" in response to save prompt). This will bundle together the current channel and feature of the npz along with a generated lineage file, which will contain label and frame information and empty parent/daughter entries for each cell. The new trk file can then be edited in Caliban's trk mode to add relationship information. @@ -125,7 +126,7 @@ In npz mode, a new npz file will be saved with a version number. An npz can be s ## Instructions for Running Caliban in a Docker Container -In addition to having Docker, you will also need to have a VNC viewer to view the application inside the container. +In addition to having Docker, you will also need to have a VNC viewer to view the application inside the container. To install one, you can go to http://realvnc.com to download a free VNC viewer. [Direct Link to Download Page](https://www.realvnc.com/en/connect/download/viewer/) @@ -142,7 +143,7 @@ docker build -t caliban . ```bash docker run \ -p 5900:5900 \ --v $PWD/caliban/desktop:/usr/src/app/desktop \ +-v $PWD/desktop:/usr/src/app/desktop \ --privileged \ caliban:latest ``` @@ -160,11 +161,9 @@ cd desktop python3 caliban.py [input file location] ``` -To see an immediate example with a sample .trk file, you can run +To see an immediate example with a sample .trk file, you can run ```bash cd desktop python3 caliban.py examples/trackfile1.trk ``` - - diff --git a/browser/.ebextensions/healthcheckurl.config b/browser/.ebextensions/healthcheckurl.config new file mode 100644 index 000000000..351dc2461 --- /dev/null +++ b/browser/.ebextensions/healthcheckurl.config @@ -0,0 +1,4 @@ +option_settings: + - namespace: aws:elasticbeanstalk:application + option_name: Application Healthcheck URL + value: /health diff --git a/browser/.ebextensions/options.config b/browser/.ebextensions/options.config index dcd000a05..71203417f 100644 --- a/browser/.ebextensions/options.config +++ b/browser/.ebextensions/options.config @@ -1,5 +1,4 @@ option_settings: - namespace: aws:elasticbeanstalk:container:python option_name: WSGIPath - value: eb_application.py - + value: application.py diff --git a/browser/.env.example b/browser/.env.example new file mode 100644 index 000000000..2b560721a --- /dev/null +++ b/browser/.env.example @@ -0,0 +1,11 @@ +# Flask settings +DEBUG= +TEMPLATES_AUTO_RELOAD= + +# AWS credentials +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= + +# SQL Alchemy settings +SQLALCHEMY_DATABASE_URI= +SQLALCHEMY_TRACK_MODIFICATIONS= diff --git a/browser/Dockerfile b/browser/Dockerfile new file mode 100644 index 000000000..3f32e06c7 --- /dev/null +++ b/browser/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.7 + +WORKDIR /usr/src/app + +COPY requirements.txt requirements.txt + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["/bin/sh", "-c", "python application.py"] diff --git a/browser/README.md b/browser/README.md index dec6fd44c..7e1e5f636 100644 --- a/browser/README.md +++ b/browser/README.md @@ -18,6 +18,32 @@ pip install -r requirements.txt ```bash python3 application.py ``` +## To use docker-compose for local development +Add your AWS credentials to ```docker-compose.yaml```. + +From the ```caliban/browser``` folder, run: +```bash +sudo docker-compose up --build -d +``` +Wait a minute for the database to finish setting up before running: +``` +sudo docker-compose restart app +``` +You can now go to 0.0.0.0:5000 in a browser window to access the local version of the tool. + +To interact with the local mysql database: +``` +sudo docker exec -it browser_db_1 bash +mysql -p +``` +When finished: +``` +sudo docker-compose down +``` +(optional) +``` +sudo docker system prune --volumes +``` ## Structure of Browser Version @@ -81,7 +107,7 @@ Keybinds in pixel editing mode are different from those in the label-editing mod Annotation mode focuses on using an adjustable brush to modify annotations on a pixel level, rather than using operations that apply to every instance of a label within a frame or set of frames. The brush tool will only make changes to the currently selected value. Ie, a brush set to edit cell 5 will only add or erase "5" to the annotated image. -*-/=* - increment value that brush is applying +*[ (left bracket) / ] (right bracket)* - increment value that brush is applying *↓ ↑* - change size of brush tool @@ -100,6 +126,10 @@ Annotation mode focuses on using an adjustable brush to modify annotations on a ### Viewing Options: +*spacebar + click and drag* - pan across canvas + +*-/= keys or alt + scroll wheel* - zoom in and out + *c* - cycle between different channels when no cells are selected *e* - toggle annotation mode (when nothing else selected) diff --git a/browser/application.py b/browser/application.py index 1aed32f9f..2b9a71a3d 100644 --- a/browser/application.py +++ b/browser/application.py @@ -1,348 +1,93 @@ -"""Flask app route handlers""" +"""Flask application entrypoint""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function -import base64 -import json -import os -import pickle -import re -import sqlite3 -import sys -import traceback +import logging -from flask import Flask, jsonify, render_template, request, redirect +from flask import Flask +from flask.logging import default_handler +from flask_cors import CORS -from helpers import is_trk_file, is_npz_file -from caliban import TrackReview, ZStackReview +from flask_compress import Compress -# Create and configure the app -app = Flask(__name__) # pylint: disable=C0103 -app.config.from_object("config") +import config +from blueprints import bp +from models import db -@app.route("/upload_file/", methods=["GET", "POST"]) -def upload_file(project_id): - ''' Upload .trk/.npz data file to AWS S3 bucket. - ''' - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) +compress = Compress() # pylint: disable=C0103 - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - state = pickle.loads(id_exists[2]) +class ReverseProxied(object): + """Enable TLS for internal requests. - # Call function in caliban.py to save data file and send to S3 bucket - if is_trk_file(id_exists[1]): - state.action_save_track() - elif is_npz_file(id_exists[1]): - state.action_save_zstack() - - # Delete id and object from database - delete_project(conn, project_id) - conn.close() - - return redirect("/") - - -@app.route("/action///", methods=["POST"]) -def action(project_id, action_type, frame): - ''' Make an edit operation to the data file and update the object - in the database. - ''' - # obtain 'info' parameter data sent by .js script - info = {k: json.loads(v) for k, v in request.values.to_dict().items()} - frame = int(frame) - - try: - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - # Perform edit operation on the data file - state.action(action_type, info) - frames_changed = state.frames_changed - info_changed = state.info_changed - - state.frames_changed = state.info_changed = False - - # Update object in local database - update_object(conn, (id_exists[1], state, project_id)) - conn.close() - - except Exception as e: - traceback.print_exc() - return jsonify({"error": str(e)}), 500 - - if info_changed: - tracks = state.readable_tracks - else: - tracks = False - - if frames_changed: - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - img_payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - else: - img_payload = False - - return jsonify({"tracks": tracks, "imgs": img_payload}) - -@app.route("/frame//") -def get_frame(frame, project_id): - ''' Serve modes of frames as pngs. Send pngs and color mappings of - cells to .js file. - ''' - frame = int(frame) - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - conn.close() - - if id_exists is None: - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - - # Obtain raw, mask, and edit mode frames - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - - # Obtain color map of the cells - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - - return jsonify(payload) - - -@app.route("/load/", methods=["POST"]) -def load(filename): - ''' Initate TrackReview/ZStackReview object and load object to database. - Send specific attributes of the object to the .js file. - ''' - conn = create_connection("caliban.db") - - print(f"Loading track at {filename}", file=sys.stderr) - - folders = re.split('__', filename) - filename = folders[len(folders) - 1] - subfolders = folders[2:len(folders)] - - subfolders = '/'.join(subfolders) - - input_bucket = folders[0] - output_bucket = folders[1] - - if is_trk_file(filename): - # Initate TrackReview object and entry in database - track_review = TrackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, track_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": track_review.max_frames, - "tracks": track_review.readable_tracks, - "dimensions": track_review.dimensions, - "project_id": project_id, - "screen_scale": track_review.scale_factor - }) - - if is_npz_file(filename): - # Initate ZStackReview object and entry in database - zstack_review = ZStackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, zstack_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": zstack_review.max_frames, - "channel_max": zstack_review.channel_max, - "feature_max": zstack_review.feature_max, - "tracks": zstack_review.readable_tracks, - "dimensions": zstack_review.dimensions, - "project_id": project_id, - "screen_scale": zstack_review.scale_factor - }) - - conn.close() - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def form(): - ''' Request HTML landing page to be rendered if user requests for - http://127.0.0.1:5000/. - ''' - return render_template('form.html') - - -@app.route('/tool', methods=['GET', 'POST']) -def tool(): - ''' Request HTML caliban tool page to be rendered after user inputs - filename in the landing page. - ''' - filename = request.form['filename'] - print(f"{filename} is filename", file=sys.stderr) - - new_filename = 'caliban-input__caliban-output__test__{}'.format( - str(filename)) - - if is_trk_file(new_filename): - return render_template('index_track.html', filename=new_filename) - if is_npz_file(new_filename): - return render_template('index_zstack.html', filename=new_filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def shortcut(filename): - ''' Request HTML caliban tool page to be rendered if user makes a URL - request to access a specific data file that has been preloaded to the - input S3 bucket (ex. http://127.0.0.1:5000/test.npz). - ''' - - if is_trk_file(filename): - return render_template('index_track.html', filename=filename) - if is_npz_file(filename): - return render_template('index_zstack.html', filename=filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 + Found in: https://stackoverflow.com/questions/30743696 + """ + def __init__(self, app): + self.app = app + def __call__(self, environ, start_response): + scheme = environ.get('HTTP_X_FORWARDED_PROTO') + if scheme: + environ['wsgi.url_scheme'] = scheme + return self.app(environ, start_response) -def create_connection(db_file): - ''' Create a database connection to a SQLite database. - ''' - conn = None - try: - conn = sqlite3.connect(db_file) - except sqlite3.Error as err: - print(err) - return conn +def initialize_logger(): + """Set up logger format and level""" + formatter = logging.Formatter( + '[%(asctime)s]:[%(levelname)s]:[%(name)s]: %(message)s') -def create_table(conn, create_table_sql): - ''' Create a table from the create_table_sql statement. - ''' - try: - cursor = conn.cursor() - cursor.execute(create_table_sql) - except sqlite3.Error as err: - print(err) + default_handler.setFormatter(formatter) + default_handler.setLevel(logging.DEBUG) + wsgi_handler = logging.StreamHandler( + stream='ext://flask.logging.wsgi_errors_stream') + wsgi_handler.setFormatter(formatter) + wsgi_handler.setLevel(logging.DEBUG) -def create_project(conn, filename, data): - ''' Create a new project in the database table. - ''' - sql = ''' INSERT INTO projects(filename, state) - VALUES(?, ?) ''' - cursor = conn.cursor() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(default_handler) - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + # 3rd party loggers + logging.getLogger('sqlalchemy').addHandler(logging.DEBUG) + logging.getLogger('botocore').setLevel(logging.INFO) + logging.getLogger('urllib3').setLevel(logging.INFO) - cursor.execute(sql, (filename, sqlite3.Binary(state_data))) - return cursor.lastrowid +def create_app(**config_overrides): + """Factory to create the Flask application""" + app = Flask(__name__) -def update_object(conn, project): - ''' Update filename, state of a project. - ''' - sql = ''' UPDATE projects - SET filename = ? , - state = ? - WHERE id = ?''' + CORS(app) - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(project[1], pickle.HIGHEST_PROTOCOL) + app.config.from_object(config) + # apply overrides + app.config.update(config_overrides) - cur = conn.cursor() - cur.execute(sql, (project[0], sqlite3.Binary(state_data), project[2])) - conn.commit() + app.wsgi_app = ReverseProxied(app.wsgi_app) + app.jinja_env.auto_reload = True -def get_project(conn, project_id): - '''Fetches TrackReview/ZStackReview object from database by project_id. + db.app = app # setting context + db.init_app(app) - Args: - conn (obj): SQL database connection. - project_id (int): The primary key of the projects table. + db.create_all() - Returns: - tuple: all data columns matching the project_id. - ''' - cur = conn.cursor() - cur.execute("SELECT * FROM {tn} WHERE {idf}={my_id}".format( - tn="projects", - idf="id", - my_id=project_id - )) - return cur.fetchone() + app.register_blueprint(bp) + compress.init_app(app) -def delete_project(conn, project_id): - ''' Delete data object (TrackReview/ZStackReview) by id. - ''' - sql = 'DELETE FROM projects WHERE id=?' - cur = conn.cursor() - cur.execute(sql, (project_id,)) - conn.commit() + return app -def main(): - ''' Runs app and initiates database file if it doesn't exist. - ''' - conn = create_connection("caliban.db") - sql_create_projects_table = """ - CREATE TABLE IF NOT EXISTS projects ( - id integer PRIMARY KEY, - filename text NOT NULL, - state blob NOT NULL - ); - """ - create_table(conn, sql_create_projects_table) - conn.commit() - conn.close() +application = create_app() # pylint: disable=C0103 - app.jinja_env.auto_reload = True - app.config['TEMPLATES_AUTO_RELOAD'] = True - app.run('0.0.0.0', port=5000) -if __name__ == "__main__": - main() +if __name__ == '__main__': + initialize_logger() + application.run('0.0.0.0', + port=application.config['PORT'], + debug=application.config['DEBUG']) diff --git a/browser/blueprints.py b/browser/blueprints.py new file mode 100644 index 000000000..0d4c64947 --- /dev/null +++ b/browser/blueprints.py @@ -0,0 +1,336 @@ +"""Flask blueprint for modular routes.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import base64 +import distutils +import distutils.util +import json +import os +import pickle +import re +import timeit +import traceback + +from flask import Blueprint +from flask import jsonify +from flask import render_template +from flask import request +from flask import redirect +from flask import current_app +from werkzeug.exceptions import HTTPException + +from helpers import is_trk_file, is_npz_file +from caliban import TrackReview, ZStackReview +from models import Project + + +bp = Blueprint('caliban', __name__) # pylint: disable=C0103 + + +def load_project_state(project): + """Unpickle the project's state into a Caliban object""" + start = timeit.default_timer() + state = pickle.loads(project.state) + current_app.logger.debug('Unpickled project "%s" state in %s s.', + project.id, timeit.default_timer() - start) + return state + + +@bp.route('/health') +def health(): + """Returns success if the application is ready.""" + return jsonify({'message': 'success'}), 200 + + +@bp.errorhandler(Exception) +def handle_exception(error): + """Handle all uncaught exceptions""" + # pass through HTTP errors + if isinstance(error, HTTPException): + return error + + current_app.logger.error('Encountered %s: %s', + error.__class__.__name__, error) + + # now you're handling non-HTTP exceptions only + return jsonify({'message': str(error)}), 500 + + +@bp.route('/upload_file/', methods=['GET', 'POST']) +def upload_file(project_id): + '''Upload .trk/.npz data file to AWS S3 bucket.''' + start = timeit.default_timer() + # Use id to grab appropriate TrackReview/ZStackReview object from database + project = Project.get_project_by_id(project_id) + + if not project: + return jsonify({'error': 'project_id not found'}), 404 + + state = load_project_state(project) + + # Call function in caliban.py to save data file and send to S3 bucket + if is_trk_file(project.filename): + state.action_save_track() + elif is_npz_file(project.filename): + state.action_save_zstack() + + # add "finished" timestamp and null out state longblob + Project.finish_project(project) + + current_app.logger.debug('Uploaded file "%s" for project "%s" in %s s.', + project.filename, project_id, + timeit.default_timer() - start) + + return redirect('/') + + +@bp.route('/action///', methods=['POST']) +def action(project_id, action_type, frame): + ''' Make an edit operation to the data file and update the object + in the database. + ''' + start = timeit.default_timer() + # obtain 'info' parameter data sent by .js script + info = {k: json.loads(v) for k, v in request.values.to_dict().items()} + + try: + # Use id to grab appropriate TrackReview/ZStackReview object from database + project = Project.get_project_by_id(project_id) + + if not project: + return jsonify({'error': 'project_id not found'}), 404 + + state = load_project_state(project) + # Perform edit operation on the data file + state.action(action_type, info) + + x_changed = state._x_changed + y_changed = state._y_changed + info_changed = state.info_changed + + state._x_changed = state._y_changed = state.info_changed = False + + # Update object in local database + Project.update_project(project, state) + + except Exception as e: # TODO: more error handling to identify problem + traceback.print_exc() + return jsonify({'error': str(e)}), 500 + + tracks = state.readable_tracks if info_changed else False + + if x_changed or y_changed: + encode = lambda x: base64.encodebytes(x.read()).decode() + img_payload = {} + + if x_changed: + raw = state.get_frame(frame, raw=True) + img_payload['raw'] = f'data:image/png;base64,{encode(raw)}' + if y_changed: + img = state.get_frame(frame, raw=False) + img_payload['segmented'] = f'data:image/png;base64,{encode(img)}' + edit_arr = state.get_array(frame) + img_payload['seg_arr'] = edit_arr.tolist() + + else: + img_payload = False + + current_app.logger.debug('Action "%s" for project "%s" finished in %s s.', + action_type, project_id, + timeit.default_timer() - start) + + return jsonify({'tracks': tracks, 'imgs': img_payload}) + + +@bp.route('/frame//') +def get_frame(frame, project_id): + ''' Serve modes of frames as pngs. Send pngs and color mappings of + cells to .js file. + ''' + start = timeit.default_timer() + # Use id to grab appropriate TrackReview/ZStackReview object from database + project = Project.get_project_by_id(project_id) + + if not project: + return jsonify({'error': 'project_id not found'}), 404 + + state = load_project_state(project) + + # Obtain raw, mask, and edit mode frames + img = state.get_frame(frame, raw=False) + raw = state.get_frame(frame, raw=True) + + # Obtain color map of the cells + edit_arr = state.get_array(frame) + + encode = lambda x: base64.encodebytes(x.read()).decode() + + payload = { + 'raw': f'data:image/png;base64,{encode(raw)}', + 'segmented': f'data:image/png;base64,{encode(img)}', + 'seg_arr': edit_arr.tolist() + } + + current_app.logger.debug('Got frame %s of project "%s" in %s s.', + frame, project_id, timeit.default_timer() - start) + + return jsonify(payload) + + +@bp.route('/load/', methods=['POST']) +def load(filename): + ''' Initate TrackReview/ZStackReview object and load object to database. + Send specific attributes of the object to the .js file. + ''' + start = timeit.default_timer() + current_app.logger.info('Loading track at %s', filename) + + folders = re.split('__', filename) + filename = folders[len(folders) - 1] + subfolders = folders[2:len(folders) - 1] + + subfolders = '/'.join(subfolders) + full_path = os.path.join(subfolders, filename) + + input_bucket = folders[0] + output_bucket = folders[1] + + if is_trk_file(filename): + # Initate TrackReview object and entry in database + track_review = TrackReview(filename, input_bucket, output_bucket, full_path) + project = Project.create_project(filename, track_review, subfolders) + current_app.logger.debug('Loaded trk file "%s" in %s s.', + filename, timeit.default_timer() - start) + # Send attributes to .js file + return jsonify({ + 'max_frames': track_review.max_frames, + 'tracks': track_review.readable_tracks, + 'dimensions': (track_review.width, track_review.height), + 'project_id': project.id, + 'screen_scale': track_review.scale_factor + }) + + if is_npz_file(filename): + # arg is 'false' which gets parsed to True if casting to bool + rgb = request.args.get('rgb', default='false', type=str) + rgb = bool(distutils.util.strtobool(rgb)) + # Initate ZStackReview object and entry in database + zstack_review = ZStackReview(filename, input_bucket, output_bucket, full_path, rgb) + project = Project.create_project(filename, zstack_review, subfolders) + current_app.logger.debug('Loaded npz file "%s" in %s s.', + filename, timeit.default_timer() - start) + # Send attributes to .js file + return jsonify({ + 'max_frames': zstack_review.max_frames, + 'channel_max': zstack_review.channel_max, + 'feature_max': zstack_review.feature_max, + 'tracks': zstack_review.readable_tracks, + 'dimensions': (zstack_review.width, zstack_review.height), + 'project_id': project.id + }) + + error = { + 'error': 'invalid file extension: {}'.format( + os.path.splitext(filename)[-1]) + } + return jsonify(error), 400 + + +@bp.route('/', methods=['GET', 'POST']) +def form(): + '''Request HTML landing page to be rendered.''' + return render_template('index.html') + + +@bp.route('/tool', methods=['GET', 'POST']) +def tool(): + ''' Request HTML caliban tool page to be rendered after user inputs + filename in the landing page. + ''' + if 'filename' not in request.form: + return redirect('/') + + filename = request.form['filename'] + + current_app.logger.info('%s is filename', filename) + + # TODO: better name template? + new_filename = 'caliban-input__caliban-output__test__{}'.format(filename) + + # if no options passed (how this route will be for now), + # still want to pass in default settings + rgb = request.args.get('rgb', default='false', type=str) + pixel_only = request.args.get('pixel_only', default='false', type=str) + label_only = request.args.get('label_only', default='false', type=str) + + # Using distutils to cast string arguments to bools + settings = { + 'rgb': bool(distutils.util.strtobool(rgb)), + 'pixel_only': bool(distutils.util.strtobool(pixel_only)), + 'label_only': bool(distutils.util.strtobool(label_only)) + } + + if is_trk_file(new_filename): + filetype = 'track' + title = 'Tracking Tool' + + elif is_npz_file(new_filename): + filetype = 'zstack' + title = 'Z-Stack Tool' + + else: + # TODO: render an error template instead of JSON. + error = { + 'error': 'invalid file extension: {}'.format( + os.path.splitext(filename)[-1]) + } + return jsonify(error), 400 + + return render_template( + 'tool.html', + filetype=filetype, + title=title, + filename=new_filename, + settings=settings) + + +@bp.route('/', methods=['GET', 'POST']) +def shortcut(filename): + ''' Request HTML caliban tool page to be rendered if user makes a URL + request to access a specific data file that has been preloaded to the + input S3 bucket (ex. http://127.0.0.1:5000/test.npz). + ''' + rgb = request.args.get('rgb', default='false', type=str) + pixel_only = request.args.get('pixel_only', default='false', type=str) + label_only = request.args.get('label_only', default='false', type=str) + + settings = { + 'rgb': bool(distutils.util.strtobool(rgb)), + 'pixel_only': bool(distutils.util.strtobool(pixel_only)), + 'label_only': bool(distutils.util.strtobool(label_only)) + } + + if is_trk_file(filename): + filetype = 'track' + title = 'Tracking Tool' + + elif is_npz_file(filename): + filetype = 'zstack' + title = 'Z-Stack Tool' + + else: + # TODO: render an error template instead of JSON. + error = { + 'error': 'invalid file extension: {}'.format( + os.path.splitext(filename)[-1]) + } + return jsonify(error), 400 + + return render_template( + 'tool.html', + filetype=filetype, + title=title, + filename=filename, + settings=settings) diff --git a/browser/blueprints_test.py b/browser/blueprints_test.py new file mode 100644 index 000000000..dbf3089e9 --- /dev/null +++ b/browser/blueprints_test.py @@ -0,0 +1,176 @@ +"""Test for Caliban Blueprints""" + +import io + +import pytest + +# from flask_sqlalchemy import SQLAlchemy + +import models + + +class Bunch(object): + def __init__(self, **kwds): + self.__dict__.update(kwds) + + +class DummyState(io.BytesIO): + + def __init__(self, *_, **__): + super().__init__() + + def __getattr__(self, *_, **__): + return self + + def __call__(self, *_, **__): + return self + + def get_frame(self, frame, raw=False): + return io.BytesIO() + + +def test_health(client, mocker): + response = client.get('/health') + assert response.status_code == 200 + assert response.json.get('message') == 'success' + + +def test_create(client): + pass + + +def test_upload_file(client): + response = client.get('/upload_file/1') + assert response.status_code == 404 + + filename_npz = 'filename.npz' + filename_trk = 'filename.trk' + state = DummyState() + subfolders = 'subfolders' + + # Create a project. + project = models.Project.create_project( + filename=filename_npz, + state=state, + subfolders=subfolders) + + response = client.get('/upload_file/{}'.format(project.id)) + assert response.status_code == 302 + + project = models.Project.create_project( + filename=filename_trk, + state=state, + subfolders=subfolders) + + response = client.get('/upload_file/{}'.format(project.id)) + assert response.status_code == 302 + + +def test_get_frame(client): + response = client.get('/frame/0/999999') + assert response.status_code == 404 + + for filename in ('filename.npz', 'filename.trk'): + + # Create a project. + project = models.Project.create_project( + filename=filename, + state=DummyState(), + subfolders='subfolders') + + response = client.get('/frame/0/{}'.format(project.id)) + + # TODO: test correctness + assert 'raw' in response.json + assert 'segmented' in response.json + assert 'seg_arr' in response.json + + # test handle error + project = models.Project.create_project( + filename=filename, + state='invalid state data', + subfolders='subfolders') + + response = client.get('/frame/0/{}'.format(project.id)) + assert response.status_code == 500 + + +def test_action(client): + pass + + +def test_load(client, mocker): + # TODO: parsing the filename is a bit awkward. + in_bucket = 'inputBucket' + out_bucket = 'inputBucket' + base_filename = 'testfile' + basefile = '{}__{}__{}__{}__{}'.format( + in_bucket, out_bucket, 'subfolder1', 'subfolder2', base_filename + ) + + mocker.patch('blueprints.TrackReview', DummyState) + mocker.patch('blueprints.ZStackReview', DummyState) + + # TODO: correctness tests + response = client.post('/load/{}.npz'.format(basefile)) + assert response.status_code == 200 + + # rgb mode only for npzs. + response = client.post('/load/{}.npz?rgb=true'.format(basefile)) + assert response.status_code == 200 + + response = client.post('/load/{}.trk'.format(basefile)) + assert response.status_code == 200 + + response = client.post('/load/{}.badext'.format(basefile)) + assert response.status_code == 400 + + +def test_tool(client): + # test no form redirect + response = client.get('/tool') + assert response.status_code == 302 + + filename = 'test-file.npz' + response = client.post('/tool', + content_type='multipart/form-data', + data={'filename': filename}) + assert response.status_code == 200 + assert b'' in response.data + + filename = 'test-file.trk' + response = client.post('/tool', + content_type='multipart/form-data', + data={'filename': filename}) + assert response.status_code == 200 + assert b'' in response.data + + filename = 'test-file.badext' + response = client.post('/tool', + content_type='multipart/form-data', + data={'filename': filename}) + assert response.status_code == 400 + assert 'error' in response.json + + +def test_shortcut(client): + options = 'rgb=true&pixel_only=true&label_only=true' + response = client.get('/test-file.npz') + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.npz?{}'.format(options)) + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.trk') + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.trk?{}'.format(options)) + assert response.status_code == 200 + assert b'' in response.data + + response = client.get('/test-file.badext') + assert response.status_code == 400 + assert 'error' in response.json diff --git a/browser/caliban.py b/browser/caliban.py index 82cf7b4a4..e81b4db23 100644 --- a/browser/caliban.py +++ b/browser/caliban.py @@ -1,205 +1,220 @@ -from io import BytesIO - -from imgutils import pngify -from matplotlib.colors import hsv_to_rgb, LinearSegmentedColormap -import matplotlib.pyplot as plt -from random import random +"""Review classes for editing np arrays""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function import io import copy import json -import matplotlib -import numpy as np import os -import random +import sys import tarfile import tempfile + import boto3 -import sys -from werkzeug.utils import secure_filename +import matplotlib.pyplot as plt +import numpy as np from skimage import filters -import skimage.morphology -from skimage.morphology import watershed, dilation, disk from skimage.morphology import flood_fill, flood +from skimage.morphology import watershed, dilation, disk from skimage.draw import circle from skimage.measure import regionprops from skimage.exposure import rescale_intensity +from skimage.segmentation import find_boundaries -from config import S3_KEY, S3_SECRET +from imgutils import pngify +from helpers import is_npz_file, is_trk_file +from config import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY -# Connect to the s3 service -s3 = boto3.client( - "s3", - aws_access_key_id=S3_KEY, - aws_secret_access_key=S3_SECRET -) -class ZStackReview: - def __init__(self, filename, input_bucket, output_bucket, subfolders): +class BaseReview(object): # pylint: disable=useless-object-inheritance + """Base class for all Review objects.""" + def __init__(self, filename, input_bucket, output_bucket, subfolders, + raw_key='raw', annotated_key='annotated'): self.filename = filename self.input_bucket = input_bucket self.output_bucket = output_bucket - self.subfolders = subfolders - self.trial = self.load(filename) - self.raw = self.trial["raw"] - self.annotated = self.trial["annotated"] + self.subfolders = subfolders # full file path + + self.current_frame = 0 + self.scale_factor = 1 + self._x_changed = False + self._y_changed = False + self.info_changed = False + + self.color_map = plt.get_cmap('viridis') + self.color_map.set_bad('black') + self.feature = 0 - self.feature_max = self.annotated.shape[-1] self.channel = 0 - self.max_frames, self.height, self.width, self.channel_max = self.raw.shape - self.dimensions = (self.width, self.height) - #create a dictionary that has frame information about each cell - #analogous to .trk lineage but do not need relationships between cells included - self.cell_ids = {} - self.num_cells = {} - self.cell_info = {} - - self.current_frame = 0 + self.trial = self.load(filename) + self.raw = self.trial[raw_key] + self.annotated = self.trial[annotated_key] - for feature in range(self.feature_max): - self.create_cell_info(feature) + self.channel_max = self.raw.shape[-1] + self.feature_max = self.annotated.shape[-1] + # TODO: is there a potential IndexError here? + self.max_frames = self.raw.shape[0] + self.height = self.raw.shape[1] + self.width = self.raw.shape[2] - self.draw_raw = False self.max_intensity = {} for channel in range(self.channel_max): self.max_intensity[channel] = None - self.dtype_raw = self.raw.dtype - self.scale_factor = 2 - - self.save_version = 0 - - self.color_map = plt.get_cmap('viridis') - self.color_map.set_bad('black') + def _get_s3_client(self): + return boto3.client( + 's3', + aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY + ) - self.frames_changed = False - self.info_changed = False + def load(self, filename): + """Load a file from the S3 input bucket""" + if is_npz_file(filename): + _load = load_npz + elif is_trk_file(filename): + _load = load_trks + else: + raise ValueError('Cannot load file: {}'.format(filename)) - @property - def readable_tracks(self): - """ - Preprocesses tracks for presentation on browser. For example, - simplifying track['frames'] into something like [0-29] instead of - [0,1,2,3,...]. + s3 = self._get_s3_client() + response = s3.get_object(Bucket=self.input_bucket, Key=self.subfolders) + return _load(response['Body'].read()) + + def rescale_95(self, img): + """Rescale a single- or multi-channel image.""" + percentiles = np.percentile(img[img > 0], [5, 95]) + rescaled_img = rescale_intensity( + img, + in_range=(percentiles[0], percentiles[1]), + out_range='uint8') + rescaled_img = rescaled_img.astype('uint8') + return rescaled_img + + def add_outlines(self, frame): + """Indicate label outlines in array with negative values of that label. """ - cell_info = copy.deepcopy(self.cell_info) - for _, feature in cell_info.items(): - for _, label in feature.items(): - slices = list(map(list, consecutive(label['frames']))) - slices = '[' + ', '.join(["{}".format(a[0]) - if len(a) == 1 else "{}-{}".format(a[0], a[-1]) - for a in slices]) + ']' - label["slices"] = str(slices) - - return cell_info + # this is sometimes int 32 but may be uint, convert to + # int16 to ensure negative numbers and smaller payload than int32 + frame = frame.astype(np.int16) + boundary_mask = find_boundaries(frame, mode='inner') + outlined_frame = np.where(boundary_mask == 1, -frame, frame) + return outlined_frame + + def get_array(self, frame, add_outlines=True): + frame = self.annotated[frame, ..., self.feature] + if add_outlines: + frame = self.add_outlines(frame) + return frame def get_frame(self, frame, raw): + self.current_frame = frame if raw: - frame = self.raw[frame][:,:, self.channel] + frame = self.raw[frame, ..., self.channel] return pngify(imgarr=frame, vmin=0, vmax=self.max_intensity[self.channel], - cmap="cubehelix") + cmap='cubehelix') else: - frame = self.annotated[frame][:,:, self.feature] + frame = self.annotated[frame, ..., self.feature] frame = np.ma.masked_equal(frame, 0) return pngify(imgarr=frame, - vmin=0, - vmax=np.max(self.cell_ids[self.feature]), - cmap=self.color_map) + vmin=0, + vmax=self.get_max_label(), + cmap=self.color_map) - def get_array(self, frame): - frame = self.annotated[frame][:,:,self.feature] - return frame + def add_cell_info(self, add_label, frame): + raise NotImplementedError('add_cell_info is not implemented in BaseReview') - def load(self, filename): + def del_cell_info(self, del_label, frame): + raise NotImplementedError('del_cell_info is not implemented in BaseReview') - global original_filename - original_filename = filename - s3 = boto3.client('s3') - key = self.subfolders - print(key) - response = s3.get_object(Bucket=self.input_bucket, Key= key) - return load_npz(response['Body'].read()) + def get_max_label(self): + raise NotImplementedError('del_cell_info is not implemented in BaseReview') def action(self, action_type, info): - - # change displayed channel or feature - if action_type == "change_channel": - self.action_change_channel(**info) - elif action_type == "change_feature": - self.action_change_feature(**info) - - # edit mode actions - elif action_type == "handle_draw": - self.action_handle_draw(**info) - elif action_type == "threshold": - self.action_threshold(**info) - - # modified click actions - elif action_type == "flood_cell": - self.action_flood_contiguous(**info) - elif action_type == "trim_pixels": - self.action_trim_pixels(**info) - - # single click actions - elif action_type == "fill_hole": - self.action_fill_hole(**info) - elif action_type == "new_single_cell": - self.action_new_single_cell(**info) - elif action_type == "new_cell_stack": - self.action_new_cell_stack(**info) - elif action_type == "delete": - self.action_delete_mask(**info) - - # multiple click actions - elif action_type == "replace_single": - self.action_replace_single(**info) - elif action_type == "replace": - self.action_replace(**info) - elif action_type == "swap_single_frame": - self.action_swap_single_frame(**info) - elif action_type == "swap_all_frame": - self.action_swap_all_frame(**info) - elif action_type == "watershed": - self.action_watershed(**info) - - # misc - elif action_type == "predict_single": - self.action_predict_single(**info) - elif action_type == "predict_zstack": - self.action_predict_zstack(**info) - - else: - raise ValueError("Invalid action '{}'".format(action_type)) + """Call an action method based on an action type.""" + attr_name = 'action_{}'.format(action_type) + try: + action = getattr(self, attr_name) + action(**info) + except AttributeError: + raise ValueError('Invalid action "{}"'.format(action_type)) def action_change_channel(self, channel): + """Change selected channel.""" + if channel < 0 or channel > self.channel_max: + raise ValueError('Channel {} is outside of range [0, {}].'.format( + channel, self.channel_max)) self.channel = channel - self.frames_changed = True + self._x_changed = True def action_change_feature(self, feature): + """Change selected feature.""" + if feature < 0 or feature > self.feature_max: + raise ValueError('Feature {} is outside of range [0, {}].'.format( + feature, self.feature_max)) self.feature = feature - self.frames_changed = True + self._y_changed = True - def action_handle_draw(self, trace, target_value, brush_value, brush_size, erase, frame): + def action_new_single_cell(self, label, frame): + """Create new label in just one frame""" + new_label = self.get_max_label() + 1 + + # replace frame labels + img = self.annotated[frame, ..., self.feature] + img[img == label] = new_label - annotated = np.copy(self.annotated[frame,:,:,self.feature]) + # replace fields + self.del_cell_info(del_label=label, frame=frame) + self.add_cell_info(add_label=new_label, frame=frame) + + def action_delete_mask(self, label, frame): + """Deletes label from the frame""" + # TODO: update the action name? + ann_img = self.annotated[frame, ..., self.feature] + ann_img = np.where(ann_img == label, 0, ann_img) + + self.annotated[frame, ..., self.feature] = ann_img + + # update cell_info + self.del_cell_info(del_label=label, frame=frame) + + def action_swap_single_frame(self, label_1, label_2, frame): + """Swap labels of two objects in the frame.""" + ann_img = self.annotated[frame, ..., self.feature] + ann_img = np.where(ann_img == label_1, -1, ann_img) + ann_img = np.where(ann_img == label_2, label_1, ann_img) + ann_img = np.where(ann_img == -1, label_2, ann_img) + + self.annotated[frame, ..., self.feature] = ann_img + + self._y_changed = self.info_changed = True + + def action_handle_draw(self, trace, target_value, brush_value, brush_size, erase, frame): + """Use a "brush" to draw in the brush value along trace locations of + the annotated data. + """ + annotated = np.copy(self.annotated[frame, ..., self.feature]) in_original = np.any(np.isin(annotated, brush_value)) - annotated_draw = np.where(annotated==target_value, brush_value, annotated) - annotated_erase = np.where(annotated==brush_value, target_value, annotated) + annotated_draw = np.where(annotated == target_value, brush_value, annotated) + annotated_erase = np.where(annotated == brush_value, target_value, annotated) for loc in trace: # each element of trace is an array with [y,x] coordinates of array x_loc = loc[1] y_loc = loc[0] - brush_area = circle(y_loc, x_loc, brush_size, (self.height,self.width)) + brush_area = circle(y_loc, x_loc, + brush_size // self.scale_factor, + (self.height, self.width)) - #do not overwrite or erase labels other than the one you're editing + # do not overwrite or erase labels other than the one you're editing if not erase: annotated[brush_area] = annotated_draw[brush_area] else: @@ -207,154 +222,320 @@ def action_handle_draw(self, trace, target_value, brush_value, brush_size, erase in_modified = np.any(np.isin(annotated, brush_value)) - #cell deletion + # cell deletion if in_original and not in_modified: - self.del_cell_info(feature = self.feature, del_label = brush_value, frame = frame) + self.del_cell_info(del_label=brush_value, frame=frame) - #cell addition + # cell addition elif in_modified and not in_original: - self.add_cell_info(feature = self.feature, add_label = brush_value, frame = frame) + self.add_cell_info(add_label=brush_value, frame=frame) - #check for image change, in case pixels changed but no new or del cell - comparison = np.where(annotated != self.annotated[frame,:,:,self.feature]) - self.frames_changed = np.any(comparison) - #if info changed, self.info_changed set to true with info helper functions + # check for image change, in case pixels changed but no new or del cell + comparison = np.where(annotated != self.annotated[frame, ..., self.feature]) + self._y_changed = np.any(comparison) + # if info changed, self.info_changed set to true with info helper functions - self.annotated[frame,:,:,self.feature] = annotated + self.annotated[frame, ..., self.feature] = annotated - def action_threshold(self, y1, x1, y2, x2, frame, label): + def action_trim_pixels(self, label, frame, x_location, y_location): + """Remove any pixels with value label that are not connected to the + selected cell in the given frame. + """ + img_ann = self.annotated[frame, ..., self.feature] + + seed_point = (y_location // self.scale_factor, + x_location // self.scale_factor) + + contig_cell = flood(image=img_ann, seed_point=seed_point) + stray_pixels = np.logical_and(np.invert(contig_cell), img_ann == label) + img_trimmed = np.where(stray_pixels, 0, img_ann) + + self._y_changed = np.any(np.where(img_trimmed != img_ann)) + self.annotated[frame, ..., self.feature] = img_trimmed + + def action_fill_hole(self, label, frame, x_location, y_location): ''' - thresholds the raw image for annotation prediction within user-determined bounding box + fill a "hole" in a cell annotation with the cell label. Doesn't check + if annotation at (y,x) is zero (hole to fill) because that logic is handled in + javascript. Just takes the click location, scales it to match the actual annotation + size, then fills the hole with label (using skimage flood_fill). connectivity = 1 + prevents hole fill from spilling out into background in some cases ''' + # rescale click location -> corresponding location in annotation array + hole_fill_seed = (y_location // self.scale_factor, x_location // self.scale_factor) + # fill hole with label + img_ann = self.annotated[frame, :, :, self.feature] + filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity=1) + self.annotated[frame, :, :, self.feature] = filled_img_ann + + # never changes info but always changes annotation + self._y_changed = True + + def action_flood_contiguous(self, label, frame, x_location, y_location): + """Flood fill a cell with a unique new label. + + Alternative to watershed for fixing duplicate labels of + non-touching objects. + """ + img_ann = self.annotated[frame, ..., self.feature] + old_label = label + new_label = self.get_max_label() + 1 + + in_original = np.any(np.isin(img_ann, old_label)) + + filled_img_ann = flood_fill(img_ann, + (int(y_location / self.scale_factor), + int(x_location / self.scale_factor)), + new_label) + self.annotated[frame, ..., self.feature] = filled_img_ann + + in_modified = np.any(np.isin(filled_img_ann, old_label)) + + # update cell info dicts since labels are changing + self.add_cell_info(add_label=new_label, frame=frame) + + if in_original and not in_modified: + self.del_cell_info(del_label=old_label, frame=frame) + + def action_watershed(self, label, frame, x1_location, y1_location, x2_location, y2_location): + """Use watershed to segment different objects""" + # Pull the label that is being split and find a new valid label + current_label = label + new_label = self.get_max_label() + 1 + + # Locally store the frames to work on + img_raw = self.raw[frame, ..., self.channel] + img_ann = self.annotated[frame, ..., self.feature] + + # Pull the 2 seed locations and store locally + # define a new seeds labeled img the same size as raw/annotation imgs + seeds_labeled = np.zeros(img_ann.shape) + + # create two seed locations + seeds_labeled[int(y1_location / self.scale_factor), + int(x1_location / self.scale_factor)] = current_label + + seeds_labeled[int(y2_location / self.scale_factor), + int(x2_location / self.scale_factor)] = new_label + + # define the bounding box to apply the transform on and select + # appropriate sections of 3 inputs (raw, seeds, annotation mask) + props = regionprops(np.squeeze(np.int32(img_ann == current_label))) + minr, minc, maxr, maxc = props[0].bbox + + # store these subsections to run the watershed on + img_sub_raw = np.copy(img_raw[minr:maxr, minc:maxc]) + img_sub_ann = np.copy(img_ann[minr:maxr, minc:maxc]) + img_sub_seeds = np.copy(seeds_labeled[minr:maxr, minc:maxc]) + + # contrast adjust the raw image to assist the transform + img_sub_raw_scaled = rescale_intensity(img_sub_raw) + + # apply watershed transform to the subsections + ws = watershed(-img_sub_raw_scaled, img_sub_seeds, + mask=img_sub_ann.astype(bool)) + + # did watershed effectively create a new label? + new_pixels = np.count_nonzero(np.logical_and( + ws == new_label, img_sub_ann == current_label)) + + # if only a few pixels split, dilate them; new label is "brightest" + # so will expand over other labels and increase area + if new_pixels < 5: + ws = dilation(ws, disk(3)) + + # ws may only leave a few pixels of old label + old_pixels = np.count_nonzero(ws == current_label) + if old_pixels < 5: + # create dilation image to prevent "dimmer" label from being eroded + # by the "brighter" label + dilated_ws = dilation(np.where(ws == current_label, ws, 0), disk(3)) + ws = np.where(dilated_ws == current_label, dilated_ws, ws) + + # only update img_sub_ann where ws has changed label + # from current_label to new_label + idx = np.logical_and(ws == new_label, img_sub_ann == current_label) + img_sub_ann = np.where(idx, ws, img_sub_ann) + + # reintegrate subsection into original mask + img_ann[minr:maxr, minc:maxc] = img_sub_ann + self.annotated[frame, ..., self.feature] = img_ann + + # update cell_info dict only if new label was created with ws + if np.any(np.isin(self.annotated[frame, ..., self.feature], new_label)): + self.add_cell_info(add_label=new_label, frame=frame) + + def action_threshold(self, y1, x1, y2, x2, frame, label): + """Threshold the raw image for annotation prediction within the + user-determined bounding box. + """ top_edge = min(y1, y2) bottom_edge = max(y1, y2) left_edge = min(x1, x2) right_edge = max(x1, x2) # pull out the selection portion of the raw frame - predict_area = self.raw[frame, top_edge:bottom_edge, left_edge:right_edge, self.channel] + predict_area = self.raw[frame, top_edge:bottom_edge, + left_edge:right_edge, self.channel] # triangle threshold picked after trying a few on one dataset # may not be the best threshold approach for other datasets! # pick two thresholds to use hysteresis thresholding strategy - threshold = filters.threshold_triangle(image = predict_area) + threshold = filters.threshold_triangle(image=predict_area) threshold_stringent = 1.10 * threshold # try to keep stray pixels from appearing - hyst = filters.apply_hysteresis_threshold(image = predict_area, low = threshold, high = threshold_stringent) + hyst = filters.apply_hysteresis_threshold(image=predict_area, + low=threshold, + high=threshold_stringent) ann_threshold = np.where(hyst, label, 0) - #put prediction in without overwriting - predict_area = self.annotated[frame, top_edge:bottom_edge, left_edge:right_edge, self.feature] + # put prediction in without overwriting + predict_area = self.annotated[frame, top_edge:bottom_edge, + left_edge:right_edge, self.feature] safe_overlay = np.where(predict_area == 0, ann_threshold, predict_area) - self.annotated[frame,top_edge:bottom_edge,left_edge:right_edge,self.feature] = safe_overlay + self.annotated[frame, top_edge:bottom_edge, + left_edge:right_edge, self.feature] = safe_overlay # don't need to update cell_info unless an annotation has been added - if np.any(np.isin(self.annotated[frame,:,:,self.feature], label)): - self.add_cell_info(feature=self.feature, add_label=label, frame = frame) + if np.any(np.isin(self.annotated[frame, ..., self.feature], label)): + self.add_cell_info(add_label=label, frame=frame) - def action_flood_contiguous(self, label, frame, x_location, y_location): - ''' - flood fill a cell with a unique new label; alternative to watershed - for fixing duplicate label issue if cells are not touching - ''' - img_ann = self.annotated[frame,:,:,self.feature] - old_label = label - new_label = np.max(self.cell_ids[self.feature]) + 1 - in_original = np.any(np.isin(img_ann, old_label)) +class ZStackReview(BaseReview): - filled_img_ann = flood_fill(img_ann, (int(y_location/self.scale_factor), int(x_location/self.scale_factor)), new_label) - self.annotated[frame,:,:,self.feature] = filled_img_ann + def __init__(self, filename, input_bucket, output_bucket, subfolders, rgb=False): + super(ZStackReview, self).__init__( + filename, input_bucket, output_bucket, subfolders, + raw_key='raw', annotated_key='annotated') - in_modified = np.any(np.isin(filled_img_ann, old_label)) + self.rgb = rgb - # update cell info dicts since labels are changing - self.add_cell_info(feature=self.feature, add_label=new_label, frame = frame) + if self.rgb: + # possible differences between single channel and rgb displays + if self.raw.ndim == 3: + self.raw = np.expand_dims(self.raw, axis=0) + self.annotated = np.expand_dims(self.annotated, axis=0) - if in_original and not in_modified: - self.del_cell_info(feature = self.feature, del_label = old_label, frame = frame) + # reassigning height/width for new shape. + self.max_frames = self.raw.shape[0] + self.height = self.raw.shape[1] + self.width = self.raw.shape[2] - def action_trim_pixels(self, label, frame, x_location, y_location): - ''' - get rid of any stray pixels of selected label; pixels of value label - that are not connected to the cell selected will be removed from annotation in that frame - ''' + self.rgb_img = self.reduce_to_RGB() - img_ann = self.annotated[frame,:,:,self.feature] - contig_cell = flood(image = img_ann, seed_point = (int(y_location/self.scale_factor), int(x_location/self.scale_factor))) - img_trimmed = np.where(np.logical_and(np.invert(contig_cell), img_ann == label), 0, img_ann) + # create a dictionary that has frame information about each cell + # analogous to .trk lineage but do not need relationships between cells included + self.cell_ids = {} + self.cell_info = {} - #check if image changed - comparison = np.where(img_trimmed != img_ann) - self.frames_changed = np.any(comparison) - #this action should never change the cell info + for feature in range(self.feature_max): + self.create_cell_info(feature) - self.annotated[frame,:,:,self.feature] = img_trimmed + def get_max_label(self): + """Get the highest label in use in currently-viewed feature. - def action_fill_hole(self, label, frame, x_location, y_location): + If feature is empty, returns 0 to prevent other functions from crashing. + """ + # check this first, np.max of empty array will crash + if len(self.cell_ids[self.feature]) == 0: + max_label = 0 + # if any labels exist in feature, find the max label + else: + max_label = int(np.max(self.cell_ids[self.feature])) + return max_label + + def rescale_raw(self): + """Rescale first 6 raw channels individually and store in memory. + + The rescaled raw array is used subsequently for image display purposes. + """ + rescaled = np.zeros((self.height, self.width, self.channel_max), dtype='uint8') + # this approach allows noise through + for channel in range(min(6, self.channel_max)): + raw_channel = self.raw[self.current_frame, ..., channel] + if np.sum(raw_channel) != 0: + rescaled[..., channel] = self.rescale_95(raw_channel) + return rescaled + + def reduce_to_RGB(self): ''' - fill a "hole" in a cell annotation with the cell label. Doesn't check - if annotation at (y,x) is zero (hole to fill) because that logic is handled in - javascript. Just takes the click location, scales it to match the actual annotation - size, then fills the hole with label (using skimage flood_fill). connectivity = 1 - prevents hole fill from spilling out into background in some cases + Go from rescaled raw array with up to 6 channels to an RGB image for display. + Handles adding in CMY channels as needed, and adjusting each channel if + viewing adjusted raw. Used to update self.rgb, which is used to display + raw current frame. ''' - # rescale click location -> corresponding location in annotation array - hole_fill_seed = (y_location // self.scale_factor, x_location // self.scale_factor) - # fill hole with label - img_ann = self.annotated[frame,:,:,self.feature] - filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity = 1) - self.annotated[frame,:,:,self.feature] = filled_img_ann + rescaled = self.rescale_raw() + # rgb starts as uint16 so it can handle values above 255 without overflow + rgb_img = np.zeros((self.height, self.width, 3), dtype='uint16') + + # for each of the channels that we have + for c in range(min(6, self.channel_max)): + # straightforward RGB -> RGB + new_channel = (rescaled[..., c]).astype('uint16') + if c < 3: + rgb_img[..., c] = new_channel + # collapse cyan to G and B + if c == 3: + rgb_img[..., 1] += new_channel + rgb_img[..., 2] += new_channel + # collapse magenta to R and B + if c == 4: + rgb_img[..., 0] += new_channel + rgb_img[..., 2] += new_channel + # collapse yellow to R and G + if c == 5: + rgb_img[..., 0] += new_channel + rgb_img[..., 1] += new_channel + + # clip values to uint8 range so it can be cast without overflow + rgb_img[..., 0:3] = np.clip(rgb_img[..., 0:3], a_min=0, a_max=255) + + return rgb_img.astype('uint8') - #never changes info but always changes annotation - self.frames_changed = True - - def action_new_single_cell(self, label, frame): + @property + def readable_tracks(self): """ - Create new label in just one frame + Preprocesses tracks for presentation on browser. For example, + simplifying track['frames'] into something like [0-29] instead of + [0,1,2,3,...]. """ - old_label, single_frame = label, frame - new_label = np.max(self.cell_ids[self.feature]) + 1 + cell_info = copy.deepcopy(self.cell_info) + for _, feature in cell_info.items(): + for _, label in feature.items(): + slices = list(map(list, consecutive(label['frames']))) + slices = '[' + ', '.join(["{}".format(a[0]) + if len(a) == 1 else "{}-{}".format(a[0], a[-1]) + for a in slices]) + ']' + label['slices'] = str(slices) - # replace frame labels - frame = self.annotated[single_frame,:,:,self.feature] - frame[frame == old_label] = new_label + return cell_info - # replace fields - self.del_cell_info(feature = self.feature, del_label = old_label, frame = single_frame) - self.add_cell_info(feature = self.feature, add_label = new_label, frame = single_frame) + def get_frame(self, frame, raw): + self.current_frame = frame + if (raw and self.rgb): + return pngify(imgarr=self.rgb_img, + vmin=None, + vmax=None, + cmap=None) + return super(ZStackReview, self).get_frame(frame, raw) def action_new_cell_stack(self, label, frame): - """ Creates new cell label and replaces original label with it in all subsequent frames """ old_label, start_frame = label, frame - new_label = np.max(self.cell_ids[self.feature]) + 1 + new_label = self.get_max_label() + 1 # replace frame labels - for frame in self.annotated[start_frame:,:,:,self.feature]: + for frame in self.annotated[start_frame:, ..., self.feature]: frame[frame == old_label] = new_label - for frame in range(self.annotated.shape[0]): - if new_label in self.annotated[frame,:,:,self.feature]: - self.del_cell_info(feature = self.feature, del_label = old_label, frame = frame) - self.add_cell_info(feature = self.feature, add_label = new_label, frame = frame) - - def action_delete_mask(self, label, frame): - ''' - remove selected annotation from frame, replacing with zeros - ''' - - ann_img = self.annotated[frame,:,:,self.feature] - ann_img = np.where(ann_img == label, 0, ann_img) - - self.annotated[frame,:,:,self.feature] = ann_img - - #update cell_info - self.del_cell_info(feature = self.feature, del_label = label, frame = frame) + for frame in range(self.max_frames): + if new_label in self.annotated[frame, ..., self.feature]: + self.del_cell_info(del_label=old_label, frame=frame) + self.add_cell_info(add_label=new_label, frame=frame) def action_replace_single(self, label_1, label_2, frame): ''' @@ -362,13 +543,13 @@ def action_replace_single(self, label_1, label_2, frame): to make sure labels are different and were selected within same frames before sending action ''' - annotated = self.annotated[frame,:,:,self.feature] + annotated = self.annotated[frame, ..., self.feature] # change annotation annotated = np.where(annotated == label_2, label_1, annotated) - self.annotated[frame,:,:,self.feature] = annotated + self.annotated[frame, ..., self.feature] = annotated # update info - self.add_cell_info(feature = self.feature, add_label = label_1, frame = frame) - self.del_cell_info(feature = self.feature, del_label = label_2, frame = frame) + self.add_cell_info(add_label=label_1, frame=frame) + self.del_cell_info(del_label=label_2, frame=frame) def action_replace(self, label_1, label_2): """ @@ -376,209 +557,127 @@ def action_replace(self, label_1, label_2): are different before sending action """ # check each frame - for frame in range(self.annotated.shape[0]): - annotated = self.annotated[frame,:,:,self.feature] + for frame in range(self.max_frames): + annotated = self.annotated[frame, ..., self.feature] # if label being replaced is present, remove it from image and update cell info dict if np.any(np.isin(annotated, label_2)): annotated = np.where(annotated == label_2, label_1, annotated) - self.annotated[frame,:,:,self.feature] = annotated - self.add_cell_info(feature = self.feature, add_label = label_1, frame = frame) - self.del_cell_info(feature = self.feature, del_label = label_2, frame = frame) - - def action_swap_single_frame(self, label_1, label_2, frame): - - ann_img = self.annotated[frame,:,:,self.feature] - ann_img = np.where(ann_img == label_1, -1, ann_img) - ann_img = np.where(ann_img == label_2, label_1, ann_img) - ann_img = np.where(ann_img == -1, label_2, ann_img) - - self.annotated[frame,:,:,self.feature] = ann_img - - self.frames_changed = self.info_changed = True + self.annotated[frame, ..., self.feature] = annotated + self.add_cell_info(add_label=label_1, frame=frame) + self.del_cell_info(del_label=label_2, frame=frame) def action_swap_all_frame(self, label_1, label_2): for frame in range(self.annotated.shape[0]): - ann_img = self.annotated[frame,:,:,self.feature] + ann_img = self.annotated[frame, ..., self.feature] ann_img = np.where(ann_img == label_1, -1, ann_img) ann_img = np.where(ann_img == label_2, label_1, ann_img) ann_img = np.where(ann_img == -1, label_2, ann_img) - self.annotated[frame,:,:,self.feature] = ann_img + self.annotated[frame, ..., self.feature] = ann_img - #update cell_info + # update cell_info cell_info_1 = self.cell_info[self.feature][label_1].copy() cell_info_2 = self.cell_info[self.feature][label_2].copy() - self.cell_info[self.feature][label_1].update({'frames': cell_info_2['frames']}) - self.cell_info[self.feature][label_2].update({'frames': cell_info_1['frames']}) - - self.frames_changed = self.info_changed = True - - def action_watershed(self, label, frame, x1_location, y1_location, x2_location, y2_location): - # Pull the label that is being split and find a new valid label - current_label = label - new_label = np.max(self.cell_ids[self.feature]) + 1 + self.cell_info[self.feature][label_1]['frames'] = cell_info_2['frames'] + self.cell_info[self.feature][label_2]['frames'] = cell_info_1['frames'] - # Locally store the frames to work on - img_raw = self.raw[frame,:,:,self.channel] - img_ann = self.annotated[frame,:,:,self.feature] - - # Pull the 2 seed locations and store locally - # define a new seeds labeled img that is the same size as raw/annotation imgs - seeds_labeled = np.zeros(img_ann.shape) - # create two seed locations - seeds_labeled[int(y1_location/self.scale_factor ), int(x1_location/self.scale_factor)]=current_label - seeds_labeled[int(y2_location/self.scale_factor ), int(x2_location/self.scale_factor )]=new_label - - # define the bounding box to apply the transform on and select appropriate sections of 3 inputs (raw, seeds, annotation mask) - props = regionprops(np.squeeze(np.int32(img_ann == current_label))) - minr, minc, maxr, maxc = props[0].bbox - - # store these subsections to run the watershed on - img_sub_raw = np.copy(img_raw[minr:maxr, minc:maxc]) - img_sub_ann = np.copy(img_ann[minr:maxr, minc:maxc]) - img_sub_seeds = np.copy(seeds_labeled[minr:maxr, minc:maxc]) - - # contrast adjust the raw image to assist the transform - img_sub_raw_scaled = rescale_intensity(img_sub_raw) - - # apply watershed transform to the subsections - ws = watershed(-img_sub_raw_scaled, img_sub_seeds, mask=img_sub_ann.astype(bool)) - - # did watershed effectively create a new label? - new_pixels = np.count_nonzero(np.logical_and(ws == new_label, img_sub_ann == current_label)) - # if only a few pixels split, dilate them; new label is "brightest" - # so will expand over other labels and increase area - if new_pixels < 5: - ws = dilation(ws, disk(3)) - - # ws may only leave a few pixels of old label - old_pixels = np.count_nonzero(ws == current_label) - if old_pixels < 5: - # create dilation image so "dimmer" label is not eroded by "brighter" label - dilated_ws = dilation(np.where(ws==current_label, ws, 0), disk(3)) - ws = np.where(dilated_ws==current_label, dilated_ws, ws) - - # only update img_sub_ann where ws has changed label from current_label to new_label - img_sub_ann = np.where(np.logical_and(ws == new_label,img_sub_ann == current_label), ws, img_sub_ann) - - # reintegrate subsection into original mask - img_ann[minr:maxr, minc:maxc] = img_sub_ann - self.annotated[frame,:,:,self.feature] = img_ann - - #update cell_info dict only if new label was created with ws - if np.any(np.isin(self.annotated[frame,:,:,self.feature], new_label)): - self.add_cell_info(feature=self.feature, add_label=new_label, frame = frame) + self._y_changed = self.info_changed = True def action_predict_single(self, frame): - ''' predicts zstack relationship for current frame based on previous frame useful for finetuning corrections one frame at a time ''' - - annotated = self.annotated[:,:,:,self.feature] current_slice = frame if current_slice > 0: prev_slice = current_slice - 1 - img = self.annotated[prev_slice,:,:,self.feature] - next_img = self.annotated[current_slice,:,:,self.feature] + img = self.annotated[prev_slice, ..., self.feature] + next_img = self.annotated[current_slice, ..., self.feature] updated_slice = predict_zstack_cell_ids(img, next_img) - #check if image changed + # check if image changed comparison = np.where(next_img != updated_slice) - self.frames_changed = np.any(comparison) + self._y_changed = np.any(comparison) - #if the image changed, update self.annotated and remake cell info - if self.frames_changed: - self.annotated[current_slice,:,:,int(self.feature)] = updated_slice - self.create_cell_info(feature = int(self.feature)) + # if the image changed, update self.annotated and remake cell info + if self._y_changed: + self.annotated[current_slice, ..., self.feature] = updated_slice + self.create_cell_info(feature=self.feature) def action_predict_zstack(self): ''' use location of cells in image to predict which annotations are different slices of the same cell ''' - - annotated = self.annotated[:,:,:,self.feature] - - for zslice in range(self.annotated.shape[0] -1): - img = self.annotated[zslice,:,:,self.feature] - - next_img = self.annotated[zslice + 1,:,:,self.feature] + for zslice in range(self.annotated.shape[0] - 1): + img = self.annotated[zslice, ..., self.feature] + next_img = self.annotated[zslice + 1, ..., self.feature] predicted_next = predict_zstack_cell_ids(img, next_img) - self.annotated[zslice + 1,:,:,self.feature] = predicted_next + self.annotated[zslice + 1, ..., self.feature] = predicted_next - #remake cell_info dict based on new annotations - self.frames_changed = True - self.create_cell_info(feature = self.feature) + # remake cell_info dict based on new annotations + self._y_changed = True + self.create_cell_info(feature=self.feature) def action_save_zstack(self): - save_file = self.filename + "_save_version_{}.npz".format(self.save_version) + # save file to BytesIO object + store_npz = io.BytesIO() - # save secure version of data before storing on regular file system - file = secure_filename(save_file) + # X and y are array names by convention + np.savez(store_npz, X=self.raw, y=self.annotated) + store_npz.seek(0) - np.savez(file, raw = self.raw, annotated = self.annotated) - path = self.subfolders - s3.upload_file(file, self.output_bucket, path) + # store npz file object in bucket/subfolders (subfolders is full path) + s3 = self._get_s3_client() + s3.upload_fileobj(store_npz, self.output_bucket, self.subfolders) - def add_cell_info(self, feature, add_label, frame): - ''' - helper function for actions that add a cell to the npz - ''' - #if cell already exists elsewhere in npz: + def add_cell_info(self, add_label, frame): + """Add a cell to the npz""" + # if cell already exists elsewhere in npz: add_label = int(add_label) try: - old_frames = self.cell_info[feature][add_label]['frames'] + old_frames = self.cell_info[self.feature][add_label]['frames'] updated_frames = np.append(old_frames, frame) updated_frames = np.unique(updated_frames).tolist() - self.cell_info[feature][add_label].update({'frames': updated_frames}) - #cell does not exist anywhere in npz: + self.cell_info[self.feature][add_label]['frames'] = updated_frames + # cell does not exist anywhere in npz: except KeyError: - self.cell_info[feature].update({add_label: {}}) - self.cell_info[feature][add_label].update({'label': str(add_label)}) - self.cell_info[feature][add_label].update({'frames': [frame]}) - self.cell_info[feature][add_label].update({'slices': ''}) + self.cell_info[self.feature][add_label] = { + 'label': str(add_label), + 'frames': [frame], + 'slices': '' + } + self.cell_ids[self.feature] = np.append(self.cell_ids[self.feature], add_label) - self.cell_ids[feature] = np.append(self.cell_ids[feature], add_label) + # if adding cell, frames and info have necessarily changed + self._y_changed = self.info_changed = True - self.num_cells[feature] += 1 - - #if adding cell, frames and info have necessarily changed - self.frames_changed = self.info_changed = True - - def del_cell_info(self, feature, del_label, frame): - ''' - helper function for actions that remove a cell from the npz - ''' - #remove cell from frame - old_frames = self.cell_info[feature][del_label]['frames'] + def del_cell_info(self, del_label, frame): + """Remove a cell from the npz""" + # remove cell from frame + old_frames = self.cell_info[self.feature][del_label]['frames'] updated_frames = np.delete(old_frames, np.where(old_frames == np.int64(frame))).tolist() - self.cell_info[feature][del_label].update({'frames': updated_frames}) + self.cell_info[self.feature][del_label]['frames'] = updated_frames - #if that was the last frame, delete the entry for that cell - if self.cell_info[feature][del_label]['frames'] == []: - del self.cell_info[feature][del_label] + # if that was the last frame, delete the entry for that cell + if self.cell_info[self.feature][del_label]['frames'] == []: + del self.cell_info[self.feature][del_label] - #also remove from list of cell_ids - ids = self.cell_ids[feature] - self.cell_ids[feature] = np.delete(ids, np.where(ids == np.int64(del_label))) + # also remove from list of cell_ids + ids = self.cell_ids[self.feature] + self.cell_ids[self.feature] = np.delete(ids, np.where(ids == np.int64(del_label))) - #if deleting cell, frames and info have necessarily changed - self.frames_changed = self.info_changed = True + # if deleting cell, frames and info have necessarily changed + self._y_changed = self.info_changed = True def create_cell_info(self, feature): - ''' - helper function for actions that make or remake the entire cell info dict - ''' + """Make or remake the entire cell info dict""" feature = int(feature) - annotated = self.annotated[:,:,:,feature] + annotated = self.annotated[..., feature] self.cell_ids[feature] = np.unique(annotated)[np.nonzero(np.unique(annotated))] - self.num_cells[feature] = int(max(self.cell_ids[feature])) - self.cell_info[feature] = {} for cell in self.cell_ids[feature]: @@ -589,58 +688,30 @@ def create_cell_info(self, feature): self.cell_info[feature][cell]['frames'] = [] for frame in range(self.annotated.shape[0]): - if cell in annotated[frame,:,:]: + if cell in annotated[frame, ...]: self.cell_info[feature][cell]['frames'].append(int(frame)) self.cell_info[feature][cell]['slices'] = '' self.info_changed = True - def create_lineage(self): - for cell in self.cell_ids[self.feature]: - self.lineage[str(cell)] = {} - cell_info = self.lineage[str(cell)] - - cell_info["label"] = int(cell) - cell_info["daughters"] = [] - cell_info["frame_div"] = None - cell_info["parent"] = None - cell_info["capped"] = False - cell_info["frames"] = self.cell_info[self.feature][cell]['frames'] - - -#_______________________________________________________________________________________________________________ - -class TrackReview: +class TrackReview(BaseReview): def __init__(self, filename, input_bucket, output_bucket, subfolders): - self.filename = filename - self.input_bucket = input_bucket - self.output_bucket = output_bucket - self.subfolders = subfolders - self.trial = self.load(filename) - self.raw = self.trial["raw"] - self.tracked = self.trial["tracked"] + super(TrackReview, self).__init__( + filename, input_bucket, output_bucket, subfolders, + raw_key='raw', annotated_key='tracked') # lineages is a list of dictionaries. There should be only a single one # when using a .trk file - if len(self.trial["lineages"]) != 1: - raise ValueError("Input file has multiple trials/lineages.") - - self.tracks = self.trial["lineages"][0] - - self.max_frames = self.raw.shape[0] - self.dimensions = self.raw.shape[1:3][::-1] - self.width, self.height = self.dimensions + if len(self.trial['lineages']) != 1: + raise ValueError('Input file has multiple trials/lineages.') + self.tracks = self.trial['lineages'][0] self.scale_factor = 2 - self.color_map = plt.get_cmap('viridis') - self.color_map.set_bad('black') - - self.current_frame = 0 - - self.frames_changed = False - self.info_changed = False + def get_max_label(self): + """Get the highest label in the lineage data.""" + return max(self.tracks) @property def readable_tracks(self): @@ -653,238 +724,53 @@ def readable_tracks(self): for _, track in tracks.items(): frames = list(map(list, consecutive(track["frames"]))) frames = '[' + ', '.join(["{}".format(a[0]) - if len(a) == 1 else "{}-{}".format(a[0], a[-1]) - for a in frames]) + ']' - track["frames"] = frames + if len(a) == 1 else "{}-{}".format(a[0], a[-1]) + for a in frames]) + ']' + track['frames'] = frames return tracks - def get_frame(self, frame, raw): - self.current_frame = frame - if raw: - frame = self.raw[frame][:,:,0] - return pngify(imgarr=frame, - vmin=0, - vmax=None, - cmap="cubehelix") - else: - frame = self.tracked[frame][:,:,0] - frame = np.ma.masked_equal(frame, 0) - return pngify(imgarr=frame, - vmin=0, - vmax=max(self.tracks), - cmap=self.color_map) - - def get_array(self, frame): - frame = self.tracked[frame][:,:,0] - return frame - - def load(self, filename): - global original_filename - original_filename = filename - s3 = boto3.client('s3') - response = s3.get_object(Bucket=self.input_bucket, Key=self.subfolders) - return load_trks(response['Body'].read()) - - def action(self, action_type, info): - - # edit mode action - if action_type == "handle_draw": - self.action_handle_draw(**info) - - # modified click actions - elif action_type == "flood_cell": - self.action_flood_contiguous(**info) - elif action_type == "trim_pixels": - self.action_trim_pixels(**info) - - # single click actions - elif action_type == "fill_hole": - self.action_fill_hole(**info) - elif action_type == "create_single_new": - self.action_new_single_cell(**info) - elif action_type == "create_all_new": - self.action_new_track(**info) - elif action_type == "delete_cell": - self.action_delete(**info) - - # multiple click actions - elif action_type == "set_parent": - self.action_set_parent(**info) - elif action_type == "replace": - self.action_replace(**info) - elif action_type == "swap_single_frame": - self.action_swap_single_frame(**info) - elif action_type == "swap_tracks": - self.action_swap_tracks(**info) - elif action_type == "watershed": - self.action_watershed(**info) - - # misc - elif action_type == "save_track": - self.action_save_track(**info) - - else: - raise ValueError("Invalid action '{}'".format(action_type)) - - def action_handle_draw(self, trace, edit_value, brush_size, erase, frame): - - annotated = np.copy(self.tracked[frame]) - - in_original = np.any(np.isin(annotated, edit_value)) - - annotated_draw = np.where(annotated==0, edit_value, annotated) - annotated_erase = np.where(annotated==edit_value, 0, annotated) - - for loc in trace: - # each element of trace is an array with [y,x] coordinates of array - x_loc = loc[1] - y_loc = loc[0] - - brush_area = circle(y_loc, x_loc, brush_size, (self.height,self.width)) - - #do not overwrite or erase labels other than the one you're editing - if not erase: - annotated[brush_area] = annotated_draw[brush_area] - else: - annotated[brush_area] = annotated_erase[brush_area] - - in_modified = np.any(np.isin(annotated, edit_value)) - - # cell deletion - if in_original and not in_modified: - self.del_cell_info(del_label = edit_value, frame = frame) - - # cell addition - elif in_modified and not in_original: - self.add_cell_info(add_label = edit_value, frame = frame) - - comparison = np.where(annotated != self.tracked[frame]) - self.frames_changed = np.any(comparison) - - self.tracked[frame] = annotated - - def action_flood_contiguous(self, label, frame, x_location, y_location): - ''' - flood fill a cell with a unique new label; alternative to watershed - for fixing duplicate label issue if cells are not touching - ''' - img_ann = self.tracked[frame,:,:,0] - old_label = label - new_label = max(self.tracks) + 1 - - in_original = np.any(np.isin(img_ann, old_label)) - - filled_img_ann = flood_fill(img_ann, (int(y_location/self.scale_factor), int(x_location/self.scale_factor)), new_label) - self.tracked[frame,:,:,0] = filled_img_ann - - in_modified = np.any(np.isin(filled_img_ann, old_label)) - - # update cell info dicts since labels are changing - self.add_cell_info(add_label=new_label, frame = frame) - - if in_original and not in_modified: - self.del_cell_info(del_label = old_label, frame = frame) - - def action_trim_pixels(self, label, frame, x_location, y_location): - ''' - get rid of any stray pixels of selected label; pixels of value label - that are not connected to the cell selected will be removed from annotation in that frame - ''' - - img_ann = self.tracked[frame,:,:,0] - contig_cell = flood(image = img_ann, seed_point = (int(y_location/self.scale_factor), int(x_location/self.scale_factor))) - img_trimmed = np.where(np.logical_and(np.invert(contig_cell), img_ann == label), 0, img_ann) - - comparison = np.where(img_trimmed != img_ann) - self.frames_changed = np.any(comparison) - - self.tracked[frame,:,:,0] = img_trimmed - - def action_fill_hole(self, label, frame, x_location, y_location): - ''' - fill a "hole" in a cell annotation with the cell label. Doesn't check - if annotation at (y,x) is zero (hole to fill) because that logic is handled in - javascript. Just takes the click location, scales it to match the actual annotation - size, then fills the hole with label (using skimage flood_fill). connectivity = 1 - prevents hole fill from spilling out into background in some cases - ''' - # rescale click location -> corresponding location in annotation array - hole_fill_seed = (y_location // self.scale_factor, x_location // self.scale_factor) - # fill hole with label - img_ann = self.tracked[frame,:,:,0] - filled_img_ann = flood_fill(img_ann, hole_fill_seed, label, connectivity = 1) - self.tracked[frame,:,:,0] = filled_img_ann - - self.frames_changed = True - - def action_new_single_cell(self, label, frame): - """ - Create new label in just one frame - """ - old_label = label - new_label = max(self.tracks) + 1 - - # replace frame labels - self.tracked[frame] = np.where(self.tracked[frame] == old_label, - new_label, self.tracked[frame]) - - # replace fields - self.del_cell_info(del_label = old_label, frame = frame) - self.add_cell_info(add_label = new_label, frame = frame) - def action_new_track(self, label, frame): - """ Replacing label - create in all subsequent frames """ old_label, start_frame = label, frame - new_label = max(self.tracks) + 1 + new_label = self.get_max_label() + 1 if start_frame != 0: # replace frame labels - for frame in self.tracked[start_frame:]: + # TODO: which frame is this meant to be? + for frame in self.annotated[start_frame:]: frame[frame == old_label] = new_label # replace fields track_old = self.tracks[old_label] track_new = self.tracks[new_label] = {} - idx = track_old["frames"].index(start_frame) + idx = track_old['frames'].index(start_frame) - frames_before = track_old["frames"][:idx] - frames_after = track_old["frames"][idx:] + frames_before = track_old['frames'][:idx] + frames_after = track_old['frames'][idx:] - track_old["frames"] = frames_before - track_new["frames"] = frames_after - track_new["label"] = new_label + track_old['frames'] = frames_before + track_new['frames'] = frames_after + track_new['label'] = new_label # only add daughters if they aren't in the same frame as the new track - track_new["daughters"] = [] - for d in track_old["daughters"]: - if start_frame not in self.tracks[d]["frames"]: - track_new["daughters"].append(d) - - track_new["frame_div"] = track_old["frame_div"] - track_new["capped"] = track_old["capped"] - track_new["parent"] = None + track_new['daughters'] = [] + for d in track_old['daughters']: + if start_frame not in self.tracks[d]['frames']: + track_new['daughters'].append(d) - track_old["daughters"] = [] - track_old["frame_div"] = None - track_old["capped"] = True + track_new['frame_div'] = track_old['frame_div'] + track_new['capped'] = track_old['capped'] + track_new['parent'] = None - self.frames_changed = self.info_changed = True - - def action_delete(self, label, frame): - """ - Deletes label from current frame only - """ - # Set frame labels to 0 - ann_img = self.tracked[frame] - ann_img = np.where(ann_img == label, 0, ann_img) - self.tracked[frame] = ann_img + track_old['daughters'] = [] + track_old['frame_div'] = None + track_old['capped'] = True - self.del_cell_info(del_label = label, frame = frame) + self._y_changed = self.info_changed = True def action_set_parent(self, label_1, label_2): """ @@ -897,16 +783,16 @@ def action_set_parent(self, label_1, label_2): first_frame_daughter = min(track_2['frames']) if last_frame_parent < first_frame_daughter: - track_1["daughters"].append(label_2) - daughters = np.unique(track_1["daughters"]).tolist() - track_1["daughters"] = daughters + track_1['daughters'].append(label_2) + daughters = np.unique(track_1['daughters']).tolist() + track_1['daughters'] = daughters - track_2["parent"] = label_1 + track_2['parent'] = label_1 - if track_1["frame_div"] is None: - track_1["frame_div"] = first_frame_daughter + if track_1['frame_div'] is None: + track_1['frame_div'] = first_frame_daughter else: - track_1["frame_div"] = min(track_1["frame_div"], first_frame_daughter) + track_1['frame_div'] = min(track_1['frame_div'], first_frame_daughter) self.info_changed = True @@ -916,130 +802,56 @@ def action_replace(self, label_1, label_2): """ # replace arrays for frame in range(self.max_frames): - annotated = self.tracked[frame] + annotated = self.annotated[frame] annotated = np.where(annotated == label_2, label_1, annotated) - self.tracked[frame] = annotated + self.annotated[frame] = annotated + # TODO: is this the same as add/remove? # replace fields track_1 = self.tracks[label_1] track_2 = self.tracks[label_2] - for d in track_1["daughters"]: - self.tracks[d]["parent"] = None + for d in track_1['daughters']: + self.tracks[d]['parent'] = None - track_1["frames"].extend(track_2["frames"]) - track_1["frames"] = sorted(set(track_1["frames"])) - track_1["daughters"] = track_2["daughters"] - track_1["frame_div"] = track_2["frame_div"] - track_1["capped"] = track_2["capped"] + track_1['frames'].extend(track_2['frames']) + track_1['frames'] = sorted(set(track_1['frames'])) + track_1['daughters'] = track_2['daughters'] + track_1['frame_div'] = track_2['frame_div'] + track_1['capped'] = track_2['capped'] del self.tracks[label_2] for _, track in self.tracks.items(): try: - track["daughters"].remove(label_2) + track['daughters'].remove(label_2) except ValueError: pass - self.frames_changed = self.info_changed = True - - def action_swap_single_frame(self, label_1, label_2, frame): - '''swap the labels of two cells in one frame, but do not - change any of the lineage information''' - - ann_img = self.tracked[frame,:,:,0] - ann_img = np.where(ann_img == label_1, -1, ann_img) - ann_img = np.where(ann_img == label_2, label_1, ann_img) - ann_img = np.where(ann_img == -1, label_2, ann_img) - - self.tracked[frame,:,:,0] = ann_img - - self.frames_changed = True + self._y_changed = self.info_changed = True def action_swap_tracks(self, label_1, label_2): def relabel(old_label, new_label): - for frame in self.tracked: + for frame in self.annotated: frame[frame == old_label] = new_label # replace fields track_new = self.tracks[new_label] = self.tracks[old_label] - track_new["label"] = new_label + track_new['label'] = new_label del self.tracks[old_label] - for d in track_new["daughters"]: - self.tracks[d]["parent"] = new_label + for d in track_new['daughters']: + self.tracks[d]['parent'] = new_label - if track_new["parent"] is not None: - parent_track = self.tracks[track_new["parent"]] - parent_track["daughters"].remove(old_label) - parent_track["daughters"].append(new_label) + if track_new['parent'] is not None: + parent_track = self.tracks[track_new['parent']] + parent_track['daughters'].remove(old_label) + parent_track['daughters'].append(new_label) relabel(label_1, -1) relabel(label_2, label_1) relabel(-1, label_2) - self.frames_changed = self.info_changed = True - - def action_watershed(self, label, frame, x1_location, y1_location, x2_location, y2_location): - - # Pull the label that is being split and find a new valid label - current_label = label - new_label = max(self.tracks) + 1 - - # Locally store the frames to work on - img_raw = self.raw[frame,:,:,0] - img_ann = self.tracked[frame,:,:,0] - - # Pull the 2 seed locations and store locally - # define a new seeds labeled img that is the same size as raw/annotation imgs - seeds_labeled = np.zeros(img_ann.shape) - - # create two seed locations - seeds_labeled[int(y1_location/self.scale_factor), - int(x1_location/self.scale_factor)] = current_label - - seeds_labeled[int(y2_location/self.scale_factor), - int(x2_location/self.scale_factor)] = new_label - - # define the bounding box to apply the transform on and select appropriate sections of 3 inputs (raw, seeds, annotation mask) - props = regionprops(np.squeeze(np.int32(img_ann == current_label))) - minr, minc, maxr, maxc = props[0].bbox - - # store these subsections to run the watershed on - img_sub_raw = np.copy(img_raw[minr:maxr, minc:maxc]) - img_sub_ann = np.copy(img_ann[minr:maxr, minc:maxc]) - img_sub_seeds = np.copy(seeds_labeled[minr:maxr, minc:maxc]) - - # contrast adjust the raw image to assist the transform - img_sub_raw_scaled = rescale_intensity(img_sub_raw) - - # apply watershed transform to the subsections - ws = watershed(-img_sub_raw_scaled, img_sub_seeds, mask=img_sub_ann.astype(bool)) - - # did watershed effectively create a new label? - new_pixels = np.count_nonzero(np.logical_and(ws == new_label, img_sub_ann == current_label)) - # if only a few pixels split, dilate them; new label is "brightest" - # so will expand over other labels and increase area - if new_pixels < 5: - ws = dilation(ws, disk(3)) - - # ws may only leave a few pixels of old label - old_pixels = np.count_nonzero(ws == current_label) - if old_pixels < 5: - # create dilation image so "dimmer" label is not eroded by "brighter" label - dilated_ws = dilation(np.where(ws==current_label, ws, 0), disk(3)) - ws = np.where(dilated_ws==current_label, dilated_ws, ws) - - # only update img_sub_ann where ws has changed label from current_label to new_label - img_sub_ann = np.where(np.logical_and(ws == new_label,img_sub_ann == current_label), - ws, img_sub_ann) - - #reintegrate subsection into original mask - img_ann[minr:maxr, minc:maxc] = img_sub_ann - self.tracked[frame,:,:,0] = img_ann - - #update cell_info dict only if new label was created with ws - if np.any(np.isin(self.tracked[frame,:,:,0], new_label)): - self.add_cell_info(add_label=new_label, frame = frame) + self._y_changed = self.info_changed = True def action_save_track(self): # clear any empty tracks before saving file @@ -1050,85 +862,84 @@ def action_save_track(self): for track in empty_tracks: del self.tracks[track] - file = secure_filename(self.filename) + # create file object in memory instead of writing to disk + trk_file_obj = io.BytesIO() - with tarfile.open(file, "w") as trks: - with tempfile.NamedTemporaryFile("w") as lineage_file: + with tarfile.open(fileobj=trk_file_obj, mode='w') as trks: + with tempfile.NamedTemporaryFile('w') as lineage_file: json.dump(self.tracks, lineage_file, indent=1) lineage_file.flush() - trks.add(lineage_file.name, "lineage.json") + trks.add(lineage_file.name, 'lineage.json') with tempfile.NamedTemporaryFile() as raw_file: np.save(raw_file, self.raw) raw_file.flush() - trks.add(raw_file.name, "raw.npy") + trks.add(raw_file.name, 'raw.npy') with tempfile.NamedTemporaryFile() as tracked_file: - np.save(tracked_file, self.tracked) + np.save(tracked_file, self.annotated) tracked_file.flush() - trks.add(tracked_file.name, "tracked.npy") + trks.add(tracked_file.name, 'tracked.npy') try: - s3.upload_file(file, self.output_bucket, self.subfolders) + # go to beginning of file object + trk_file_obj.seek(0) + s3 = self._get_s3_client() + s3.upload_fileobj(trk_file_obj, self.output_bucket, self.subfolders) except Exception as e: - print("Something Happened: ", e, file=sys.stderr) + print('Something Happened: ', e, file=sys.stderr) raise - #os.remove(file) - return "Success!" - def add_cell_info(self, add_label, frame): - ''' - helper function for actions that add a cell to the trk - ''' - #if cell already exists elsewhere in trk: + """Add a cell to the trk""" + # if cell already exists elsewhere in trk: + add_label = int(add_label) try: old_frames = self.tracks[add_label]['frames'] updated_frames = np.append(old_frames, frame) updated_frames = np.unique(updated_frames).tolist() - self.tracks[add_label].update({'frames': updated_frames}) - #cell does not exist anywhere in trk: + self.tracks[add_label]['frames'] = updated_frames + # cell does not exist anywhere in trk: except KeyError: - self.tracks.update({add_label: {}}) - self.tracks[add_label].update({'label': int(add_label)}) - self.tracks[add_label].update({'frames': [frame]}) - self.tracks[add_label].update({'daughters': []}) - self.tracks[add_label].update({'frame_div': None}) - self.tracks[add_label].update({'parent': None}) - self.tracks[add_label].update({'capped': False}) + self.tracks[add_label] = { + 'label': int(add_label), + 'frames': [frame], + 'daughters': [], + 'frame_div': None, + 'parent': None, + 'capped': False, + } - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def del_cell_info(self, del_label, frame): - ''' - helper function for actions that remove a cell from the trk - ''' - #remove cell from frame + """Remove a cell from the trk""" + # remove cell from frame old_frames = self.tracks[del_label]['frames'] updated_frames = np.delete(old_frames, np.where(old_frames == np.int64(frame))).tolist() - self.tracks[del_label].update({'frames': updated_frames}) + self.tracks[del_label]['frames'] = updated_frames - #if that was the last frame, delete the entry for that cell + # if that was the last frame, delete the entry for that cell if self.tracks[del_label]['frames'] == []: del self.tracks[del_label] # If deleting lineage data, remove parent/daughter entries for _, track in self.tracks.items(): try: - track["daughters"].remove(del_label) + track['daughters'].remove(del_label) except ValueError: pass - if track["parent"] == del_label: - track["parent"] = None + if track['parent'] == del_label: + track['parent'] = None - self.frames_changed = self.info_changed = True + self._y_changed = self.info_changed = True def consecutive(data, stepsize=1): - return np.split(data, np.where(np.diff(data) != stepsize)[0]+1) + return np.split(data, np.where(np.diff(data) != stepsize)[0] + 1) -def predict_zstack_cell_ids(img, next_img, threshold = 0.1): +def predict_zstack_cell_ids(img, next_img, threshold=0.1): ''' Predict labels for next_img based on intersection over union (iou) with img. If cells don't meet threshold for iou, they don't count as @@ -1141,66 +952,66 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): # relabel to remove skipped values, keeps subsequent predictions cleaner next_img = relabel_frame(next_img) - #create np array that can hold all pairings between cells in one - #image and cells in next image - iou = np.zeros((np.max(img)+1, np.max(next_img)+1)) + # create np array that can hold all pairings between cells in one + # image and cells in next image + iou = np.zeros((np.max(img) + 1, np.max(next_img) + 1)) vals = np.unique(img) cells = vals[np.nonzero(vals)] - #nothing to predict off of + # nothing to predict off of if len(cells) == 0: return next_img next_vals = np.unique(next_img) next_cells = next_vals[np.nonzero(next_vals)] - #no values to reassign + # no values to reassign if len(next_cells) == 0: return next_img - #calculate IOUs + # calculate IOUs for i in cells: for j in next_cells: - intersection = np.logical_and(img==i,next_img==j) - union = np.logical_or(img==i,next_img==j) - iou[i,j] = intersection.sum(axis=(0,1)) / union.sum(axis=(0,1)) + intersection = np.logical_and(img == i, next_img == j) + union = np.logical_or(img == i, next_img == j) + iou[i, j] = intersection.sum(axis=(0, 1)) / union.sum(axis=(0, 1)) - #relabel cells appropriately + # relabel cells appropriately - #relabeled_next holds cells as they get relabeled appropriately - relabeled_next = np.zeros(next_img.shape, dtype = np.uint16) + # relabeled_next holds cells as they get relabeled appropriately + relabeled_next = np.zeros(next_img.shape, dtype=np.uint16) - #max_indices[cell_from_next_img] -> cell from first image that matches it best - max_indices = np.argmax(iou, axis = 0) + # max_indices[cell_from_next_img] -> cell from first image that matches it best + max_indices = np.argmax(iou, axis=0) - #put cells that into new image if they've been matched with another cell + # put cells that into new image if they've been matched with another cell - #keep track of which (next_img)cells don't have matches - #this can be if (next_img)cell matched background, or if (next_img)cell matched - #a cell already used + # keep track of which (next_img)cells don't have matches + # this can be if (next_img)cell matched background, or if (next_img)cell matched + # a cell already used unmatched_cells = [] - #don't reuse cells (if multiple cells in next_img match one particular cell) + # don't reuse cells (if multiple cells in next_img match one particular cell) used_cells_src = [] - #next_cell ranges between 0 and max(next_img) - #matched_cell is which cell in img matched next_cell the best + # next_cell ranges between 0 and max(next_img) + # matched_cell is which cell in img matched next_cell the best # this for loop does the matching between cells for next_cell, matched_cell in enumerate(max_indices): - #if more than one match, look for best match - #otherwise the first match gets linked together, not necessarily reproducible + # if more than one match, look for best match + # otherwise the first match gets linked together, not necessarily reproducible # matched_cell != 0 prevents adding the background to used_cells_src if matched_cell != 0 and matched_cell not in used_cells_src: bool_matches = np.where(max_indices == matched_cell) count_matches = np.count_nonzero(bool_matches) if count_matches > 1: - #for a given cell in img, which next_cell has highest iou - matching_next_options = np.argmax(iou, axis =1) + # for a given cell in img, which next_cell has highest iou + matching_next_options = np.argmax(iou, axis=1) best_matched_next = matching_next_options[matched_cell] - #ignore if best_matched_next is the background + # ignore if best_matched_next is the background if best_matched_next != 0: if next_cell != best_matched_next: unmatched_cells = np.append(unmatched_cells, next_cell) @@ -1208,9 +1019,11 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): else: # don't add if bad match if iou[matched_cell][best_matched_next] > threshold: - relabeled_next = np.where(next_img == best_matched_next, matched_cell, relabeled_next) + relabeled_next = np.where(next_img == best_matched_next, + matched_cell, relabeled_next) - # if it's a bad match, we still need to add next_cell back into relabeled next later + # if it's a bad match, we still need to add next_cell back + # into relabeled next later elif iou[matched_cell][best_matched_next] <= threshold: unmatched_cells = np.append(unmatched_cells, best_matched_next) @@ -1219,7 +1032,7 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): # matched_cell != 0 is still true elif count_matches == 1: - #add the matched cell to the relabeled image + # add the matched cell to the relabeled image if iou[matched_cell][next_cell] > threshold: relabeled_next = np.where(next_img == next_cell, matched_cell, relabeled_next) else: @@ -1228,22 +1041,23 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): used_cells_src = np.append(used_cells_src, matched_cell) elif matched_cell in used_cells_src and next_cell != 0: - #skip that pairing, add next_cell to unmatched_cells + # skip that pairing, add next_cell to unmatched_cells unmatched_cells = np.append(unmatched_cells, next_cell) - #if the cell in next_img didn't match anything (and is not the background): - if matched_cell == 0 and next_cell !=0: + # if the cell in next_img didn't match anything (and is not the background): + if matched_cell == 0 and next_cell != 0: unmatched_cells = np.append(unmatched_cells, next_cell) - #note: this also puts skipped (nonexistent) labels into unmatched cells, main reason to relabel first + # note: this also puts skipped (nonexistent) labels into unmatched cells, + # main reason to relabel first - #figure out which labels we should use to label remaining, unmatched cells + # figure out which labels we should use to label remaining, unmatched cells - #these are the values that have already been used in relabeled_next + # these are the values that have already been used in relabeled_next relabeled_values = np.unique(relabeled_next)[np.nonzero(np.unique(relabeled_next))] - #to account for any new cells that appear, create labels by adding to the max number of cells - #assumes that these are new cells and that all prev labels have been assigned - #only make as many new labels as needed + # to account for any new cells that appear, create labels by adding to the max number of cells + # assumes that these are new cells and that all prev labels have been assigned + # only make as many new labels as needed current_max = max(np.max(cells), np.max(relabeled_values)) + 1 @@ -1252,26 +1066,26 @@ def predict_zstack_cell_ids(img, next_img, threshold = 0.1): stringent_allowed.append(current_max) current_max += 1 - #replace each unmatched cell with a value from the stringent_allowed list, - #add that relabeled cell to relabeled_next + # replace each unmatched cell with a value from the stringent_allowed list, + # add that relabeled cell to relabeled_next if len(unmatched_cells) > 0: for reassigned_cell in range(len(unmatched_cells)): relabeled_next = np.where(next_img == unmatched_cells[reassigned_cell], - stringent_allowed[reassigned_cell], relabeled_next) + stringent_allowed[reassigned_cell], relabeled_next) return relabeled_next -def relabel_frame(img, start_val = 1): +def relabel_frame(img, start_val=1): '''relabel cells in frame starting from 1 without skipping values''' - #cells in image to be relabeled + # cells in image to be relabeled cell_list = np.unique(img) cell_list = cell_list[np.nonzero(cell_list)] - relabeled_cell_list = range(start_val, len(cell_list)+start_val) + relabeled_cell_list = range(start_val, len(cell_list) + start_val) - relabeled_img = np.zeros(img.shape, dtype = np.uint16) + relabeled_img = np.zeros(img.shape, dtype=np.uint16) for i, cell in enumerate(cell_list): relabeled_img = np.where(img == cell, relabeled_cell_list[i], relabeled_img) @@ -1280,24 +1094,30 @@ def relabel_frame(img, start_val = 1): def load_npz(filename): - data = BytesIO(filename) + data = io.BytesIO(filename) npz = np.load(data) + # standard nomenclature for image (X) and annotation (y) if 'y' in npz.files: raw_stack = npz['X'] annotation_stack = npz['y'] + # some files may have alternate names 'raw' and 'annotated' elif 'raw' in npz.files: raw_stack = npz['raw'] annotation_stack = npz['annotated'] + + # if files are named something different, give it a try anyway else: raw_stack = npz[npz.files[0]] annotation_stack = npz[npz.files[1]] - return {"raw": raw_stack, "annotated": annotation_stack} + return {'raw': raw_stack, 'annotated': annotation_stack} + # copied from: # vanvalenlab/deepcell-tf/blob/master/deepcell/utils/tracking_utils.py3 + def load_trks(trkfile): """Load a trk/trks file. Args: @@ -1310,33 +1130,31 @@ def load_trks(trkfile): with tarfile.open(temp.name, 'r') as trks: # numpy can't read these from disk... - array_file = BytesIO() + array_file = io.BytesIO() array_file.write(trks.extractfile('raw.npy').read()) array_file.seek(0) raw = np.load(array_file) array_file.close() - array_file = BytesIO() + array_file = io.BytesIO() array_file.write(trks.extractfile('tracked.npy').read()) array_file.seek(0) tracked = np.load(array_file) array_file.close() - # trks.extractfile opens a file in bytes mode, json can't use bytes. - __, file_extension = os.path.splitext(original_filename) - - if file_extension == '.trks': + try: trk_data = trks.getmember('lineages.json') - lineages = json.loads(trks.extractfile(trk_data).read().decode()) - # JSON only allows strings as keys, so convert them back to ints - for i, tracks in enumerate(lineages): - lineages[i] = {int(k): v for k, v in tracks.items()} - - elif file_extension == '.trk': - trk_data = trks.getmember('lineage.json') - lineage = json.loads(trks.extractfile(trk_data).read().decode()) - # JSON only allows strings as keys, so convert them back to ints - lineages = [] - lineages.append({int(k): v for k, v in lineage.items()}) + except KeyError: + try: + trk_data = trks.getmember('lineage.json') + except KeyError: + raise ValueError('Invalid .trk file, no lineage data found.') + + lineages = json.loads(trks.extractfile(trk_data).read().decode()) + lineages = lineages if isinstance(lineages, list) else [lineages] + + # JSON only allows strings as keys, so convert them back to ints + for i, tracks in enumerate(lineages): + lineages[i] = {int(k): v for k, v in tracks.items()} return {'lineages': lineages, 'raw': raw, 'tracked': tracked} diff --git a/browser/config.py b/browser/config.py index cfff3c7a2..425863f90 100644 --- a/browser/config.py +++ b/browser/config.py @@ -1,16 +1,29 @@ -import os -import sys +"""Configuration options and environment variables.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function from decouple import config -DEBUG = True -PORT = 5000 -S3_KEY = config('S3_KEY') -S3_SECRET = config('S3_SECRET') +DEBUG = config('DEBUG', cast=bool, default=True) +PORT = config('PORT', cast=int, default=5000) -MYSQL_USERNAME = config('MYSQL_USERNAME') -MYSQL_HOSTNAME = config('MYSQL_HOSTNAME') -MYSQL_PORT = config('MYSQL_PORT', cast = int, default = 3306) -MYSQL_PASSWORD = config('MYSQL_PASSWORD') -MYSQL_DATABASE = config('MYSQL_DATABASE', default = 'caliban') +AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID', default='') +AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY', default='') + +TEMPLATES_AUTO_RELOAD = config('TEMPLATES_AUTO_RELOAD', cast=bool, default=True) + +# SQLAlchemy settings +SQLALCHEMY_TRACK_MODIFICATIONS = config('SQLALCHEMY_TRACK_MODIFICATIONS', + cast=bool, default=False) + +SQLALCHEMY_DATABASE_URI = config('SQLALCHEMY_DATABASE_URI', + default='sqlite:////tmp/caliban.db') + +# Compression settings +COMPRESS_MIMETYPES = ['text/html', 'text/css', 'text/xml', + 'application/json', 'application/javascript'] +COMPRESS_LEVEL = 6 +COMPRESS_MIN_SIZE = 500 +COMPRESS_ALGORITHM = 'gzip' diff --git a/browser/conftest.py b/browser/conftest.py new file mode 100644 index 000000000..e610bc82c --- /dev/null +++ b/browser/conftest.py @@ -0,0 +1,42 @@ +"""Tests for the Caliban Flask App.""" + +import os + +from flask_sqlalchemy import SQLAlchemy + +import pytest + +from application import create_app # pylint: disable=C0413 + +# flask-sqlalchemy fixtures from http://alexmic.net/flask-sqlalchemy-pytest/ + + +TESTDB_PATH = '/tmp/test_project.db' +TEST_DATABASE_URI = 'sqlite:///{}'.format(TESTDB_PATH) + + +@pytest.fixture +def app(): + """Session-wide test `Flask` application.""" + + if os.path.exists(TESTDB_PATH): + os.unlink(TESTDB_PATH) + + yield create_app( + TESTING=True, + SQLALCHEMY_DATABASE_URI=TEST_DATABASE_URI, + ) + + os.unlink(TESTDB_PATH) + + +@pytest.fixture +def _db(app): + """ + Provide the transactional fixtures with access to the database via a Flask-SQLAlchemy + database connection. + + https://pypi.org/project/pytest-flask-sqlalchemy/ + """ + db = SQLAlchemy(app=app) + return db diff --git a/browser/docker-compose.yaml b/browser/docker-compose.yaml new file mode 100644 index 000000000..f6a77ff16 --- /dev/null +++ b/browser/docker-compose.yaml @@ -0,0 +1,25 @@ +version: "2" +services: + app: + build: . + links: + - db + ports: + - 5000:5000 + depends_on: + - db + environment: + AWS_ACCESS_KEY_ID: '' + AWS_SECRET_ACCESS_KEY: '' + SQLALCHEMY_DATABASE_URI: mysql://root:password@db:3306/caliban + db: + image: mysql:8.0 + command: mysqld --default-authentication-plugin=mysql_native_password + ports: + - 3306:3306 + environment: + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: caliban + volumes: + - ./db:/docker-entrypoint-initdb.d/:rw diff --git a/browser/eb_application.py b/browser/eb_application.py deleted file mode 100644 index b7e4fee60..000000000 --- a/browser/eb_application.py +++ /dev/null @@ -1,357 +0,0 @@ -"""Flask app route handlers""" - -import base64 -import json -import os -import pickle -import re -import sys -import traceback - -import MySQLdb - -from flask import Flask, jsonify, render_template, request, redirect - -from helpers import is_trk_file, is_npz_file -from caliban import TrackReview, ZStackReview -import config - -# Create and configure the app -application = Flask(__name__) # pylint: disable=C0103 -app = application -app.config.from_object("config") - - -@app.route("/upload_file/", methods=["GET", "POST"]) -def upload_file(project_id): - ''' Upload .trk/.npz data file to AWS S3 bucket. - ''' - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - - # Call function in caliban.py to save data file and send to S3 bucket - if is_trk_file(id_exists[1]): - state.action_save_track() - elif is_npz_file(id_exists[1]): - state.action_save_zstack() - - # Delete id and object from database - delete_project(conn, project_id) - conn.close() - - return redirect("/") - - -@app.route("/action///", methods=["POST"]) -def action(project_id, action_type, frame): - ''' Make an edit operation to the data file and update the object - in the database. - ''' - - # obtain 'info' parameter data sent by .js script - info = {k: json.loads(v) for k, v in request.values.to_dict().items()} - frame = int(frame) - - try: - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - - if id_exists is None: - conn.close() - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - # Perform edit operation on the data file - state.action(action_type, info) - frames_changed = state.frames_changed - info_changed = state.info_changed - - state.frames_changed = state.info_changed = False - - # Update object in local database - update_object(conn, (id_exists[1], state, project_id)) - conn.close() - - except Exception as e: - traceback.print_exc() - return jsonify({"error": str(e)}), 500 - - if info_changed: - tracks = state.readable_tracks - else: - tracks = False - - if frames_changed: - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - img_payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - else: - img_payload = False - - return jsonify({"tracks": tracks, "imgs": img_payload}) - -@app.route("/frame//") -def get_frame(frame, project_id): - ''' Serve modes of frames as pngs. Send pngs and color mappings of - cells to .js file. - ''' - frame = int(frame) - conn = create_connection("caliban.db") - # Use id to grab appropriate TrackReview/ZStackReview object from database - id_exists = get_project(conn, project_id) - conn.close() - - if id_exists is None: - return jsonify({'error': 'project_id not found'}), 404 - - state = pickle.loads(id_exists[2]) - - # Obtain raw, mask, and edit mode frames - img = state.get_frame(frame, raw=False) - raw = state.get_frame(frame, raw=True) - - # Obtain color map of the cells - edit_arr = state.get_array(frame) - - encode = lambda x: base64.encodebytes(x.read()).decode() - - payload = { - 'raw': f'data:image/png;base64,{encode(raw)}', - 'segmented': f'data:image/png;base64,{encode(img)}', - 'seg_arr': edit_arr.tolist() - } - - return jsonify(payload) - -@app.route("/load/", methods=["POST"]) -def load(filename): - ''' Initate TrackReview/ZStackReview object and load object to database. - Send specific attributes of the object to the .js file. - ''' - conn = create_connection("caliban.db") - - print(f"Loading track at {filename}", file=sys.stderr) - - folders = re.split('__', filename) - filename = folders[len(folders) - 1] - subfolders = folders[2:len(folders)] - - subfolders = '/'.join(subfolders) - - input_bucket = folders[0] - output_bucket = folders[1] - - if is_trk_file(filename): - # Initate TrackReview object and entry in database - track_review = TrackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, track_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": track_review.max_frames, - "tracks": track_review.readable_tracks, - "dimensions": track_review.dimensions, - "project_id": project_id, - "screen_scale": track_review.scale_factor - }) - - if is_npz_file(filename): - # Initate ZStackReview object and entry in database - zstack_review = ZStackReview(filename, input_bucket, output_bucket, subfolders) - project_id = create_project(conn, filename, zstack_review) - conn.commit() - conn.close() - - # Send attributes to .js file - return jsonify({ - "max_frames": zstack_review.max_frames, - "channel_max": zstack_review.channel_max, - "feature_max": zstack_review.feature_max, - "tracks": zstack_review.readable_tracks, - "dimensions": zstack_review.dimensions, - "project_id": project_id, - "screen_scale": zstack_review.scale_factor - }) - - conn.close() - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def form(): - ''' Request HTML landing page to be rendered if user requests for - http://127.0.0.1:5000/. - ''' - return render_template('form.html') - - -@app.route('/tool', methods=['GET', 'POST']) -def tool(): - ''' Request HTML caliban tool page to be rendered after user inputs - filename in the landing page. - ''' - filename = request.form['filename'] - print(f"{filename} is filename", file=sys.stderr) - - new_filename = 'caliban-input__caliban-output__test__{}'.format( - str(filename)) - - if is_trk_file(new_filename): - return render_template('index_track.html', filename=new_filename) - if is_npz_file(new_filename): - return render_template('index_zstack.html', filename=new_filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - - -@app.route('/', methods=['GET', 'POST']) -def shortcut(filename): - ''' Request HTML caliban tool page to be rendered if user makes a URL - request to access a specific data file that has been preloaded to the - input S3 bucket (ex. http://127.0.0.1:5000/test.npz). - ''' - - if is_trk_file(filename): - return render_template('index_track.html', filename=filename) - if is_npz_file(filename): - return render_template('index_zstack.html', filename=filename) - - error = { - 'error': 'invalid file extension: {}'.format( - os.path.splitext(filename)[-1]) - } - return jsonify(error), 400 - -def create_connection(_): - ''' Create a database connection to a SQLite database. - ''' - conn = None - try: - conn = MySQLdb.connect( - user=config.MYSQL_USERNAME, - host=config.MYSQL_HOSTNAME, - port=config.MYSQL_PORT, - passwd=config.MYSQL_PASSWORD, - db=config.MYSQL_DATABASE, - charset='utf8', - use_unicode=True) - except MySQLdb._exceptions.MySQLError as err: - print(err) - return conn - - -def create_table(conn, create_table_sql): - ''' Create a table from the create_table_sql statement. - ''' - try: - cursor = conn.cursor() - cursor.execute(create_table_sql) - except MySQLdb._exceptions.MySQLError as err: - print(err) - - -def create_project(conn, filename, data): - ''' Create a new project in the database table. - ''' - sql = ''' INSERT INTO projects(filename, state) - VALUES(%s, %s) ''' - cursor = conn.cursor() - - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - - cursor.execute(sql, (filename, state_data)) - return cursor.lastrowid - - -def update_object(conn, project): - ''' Update filename, state of a project. - ''' - sql = ''' UPDATE projects - SET filename = %s , - state = %s - WHERE id = %s''' - - # convert object to binary data to be stored as data type BLOB - state_data = pickle.dumps(project[1], pickle.HIGHEST_PROTOCOL) - - cur = conn.cursor() - cur.execute(sql, (project[0], state_data, project[2])) - conn.commit() - - -def get_project(conn, project_id): - '''Fetches TrackReview/ZStackReview object from database by project_id. - - Args: - conn (obj): SQL database connection. - project_id (int): The primary key of the projects table. - - Returns: - tuple: all data columns matching the project_id. - ''' - cur = conn.cursor() - cur.execute("SELECT * FROM {tn} WHERE {idf}={my_id}".format( - tn="projects", - idf="id", - my_id=project_id - )) - return cur.fetchone() - - -def delete_project(conn, project_id): - ''' Delete data object (TrackReview/ZStackReview) by id. - ''' - sql = 'DELETE FROM projects WHERE id=%s' - cur = conn.cursor() - cur.execute(sql, (project_id,)) - conn.commit() - - -def main(): - ''' Runs app and initiates database file if it doesn't exist. - ''' - conn = create_connection("caliban.db") - sql_create_projects_table = """ - CREATE TABLE IF NOT EXISTS projects ( - id integer NOT NULL AUTO_INCREMENT PRIMARY KEY, - filename text NOT NULL, - state longblob NOT NULL - ); - """ - create_table(conn, sql_create_projects_table) - conn.commit() - conn.close() - - app.jinja_env.auto_reload = True - app.config['TEMPLATES_AUTO_RELOAD'] = True - app.run('0.0.0.0', port=5000) - -if __name__ == "__main__": - main() diff --git a/browser/env.example b/browser/env.example deleted file mode 100644 index a1e5fc325..000000000 --- a/browser/env.example +++ /dev/null @@ -1,10 +0,0 @@ -# AWS credentials -S3_KEY= -S3_SECRET= - -# MySQL database credentials -MYSQL_USERNAME= -MYSQL_HOSTNAME= -MYSQL_PORT= -MYSQL_PASSWORD= -MYSQL_DATABASE= \ No newline at end of file diff --git a/browser/helpers.py b/browser/helpers.py index 60f65d481..9f2b0b4c1 100644 --- a/browser/helpers.py +++ b/browser/helpers.py @@ -1,10 +1,12 @@ import os + ALLOWED_EXTENSIONS = set([ '.txt', '.md', '.markdown', '.pdf', '.png', '.jpg', '.jpeg', '.gif', ]) + def allowed_file(name): return os.path.splitext(str(name).lower())[-1] in ALLOWED_EXTENSIONS diff --git a/browser/helpers_test.py b/browser/helpers_test.py new file mode 100644 index 000000000..9196cf327 --- /dev/null +++ b/browser/helpers_test.py @@ -0,0 +1,42 @@ +"""Tests for helpers.py""" + +import pytest + +import helpers + + +def test_allowed_file(mocker): + extensions = set(['.png']) + mocker.patch('helpers.ALLOWED_EXTENSIONS', extensions) + + assert helpers.allowed_file('test.png') + assert helpers.allowed_file('test.PnG') + assert not helpers.allowed_file('test.pdf') + assert not helpers.allowed_file('this is just a string') + assert not helpers.allowed_file(1234) + assert not helpers.allowed_file(None) + assert not helpers.allowed_file(dict()) + + +def test_is_trk_file(): + assert helpers.is_trk_file('test.trk') + assert helpers.is_trk_file('test.trks') + assert helpers.is_trk_file('test.TrKs') + assert helpers.is_trk_file('test.TRk') + assert not helpers.is_trk_file('test.pdf') + assert not helpers.is_trk_file('test.npz') + assert not helpers.is_trk_file('this is just a string') + assert not helpers.is_trk_file(1234) + assert not helpers.is_trk_file(None) + assert not helpers.is_trk_file(dict()) + + +def test_is_npz_file(): + assert helpers.is_npz_file('test.npz') + assert helpers.is_npz_file('test.NpZ') + assert not helpers.is_npz_file('test.pdf') + assert not helpers.is_npz_file('test.trk') + assert not helpers.is_npz_file('this is just a string') + assert not helpers.is_npz_file(1234) + assert not helpers.is_npz_file(None) + assert not helpers.is_npz_file(dict()) diff --git a/browser/imgutils.py b/browser/imgutils.py index a9cfbdc78..7a511988c 100644 --- a/browser/imgutils.py +++ b/browser/imgutils.py @@ -1,13 +1,22 @@ +"""Utilities for handling images""" import io + import matplotlib.pyplot as plt +from matplotlib.colors import Normalize + +from PIL import Image -def pngify(imgarr, vmin, vmax, cmap): +def pngify(imgarr, vmin, vmax, cmap=None): out = io.BytesIO() - plt.imsave(out, imgarr, - vmin=vmin, - vmax=vmax, - cmap=cmap, - format="png") + + if cmap: + cmap = plt.get_cmap(cmap) + imgarr = Normalize(vmin=vmin, vmax=vmax)(imgarr) + # apply the colormap + imgarr = cmap(imgarr, bytes=True) + + img = Image.fromarray(imgarr) + img.save(out, format="png") out.seek(0) return out diff --git a/browser/imgutils_test.py b/browser/imgutils_test.py new file mode 100644 index 000000000..1cb1b1d67 --- /dev/null +++ b/browser/imgutils_test.py @@ -0,0 +1,42 @@ +"""Tests for helpers.py""" + +import os + +from skimage.io import imread +import numpy as np +import matplotlib.pyplot as plt + +import pytest + +import imgutils + + +def test_pngify(tmpdir): + outfile = os.path.join(str(tmpdir), 'output.png') + imgarr = np.random.randint(0, 255, size=(32, 32), dtype='uint16') + + # test vmin, vmax, and cmap all None + out = imgutils.pngify(imgarr, None, None, cmap=None) + with open(outfile, 'wb') as f: + f.write(out.getbuffer()) + + loaded_image = np.uint16(imread(outfile)) + np.testing.assert_equal(imgarr, loaded_image) + + # test vmin, vmax + out = imgutils.pngify(imgarr, 0, imgarr.max(), cmap=None) + with open(outfile, 'wb') as f: + f.write(out.getbuffer()) + + loaded_image = np.uint16(imread(outfile)) + np.testing.assert_equal(imgarr, loaded_image) + + # test vmin, vmax and cmap + cmap = 'cubehelix' + out = imgutils.pngify(imgarr, 0, imgarr.max(), cmap=cmap) + with open(outfile, 'wb') as f: + f.write(out.getbuffer()) + + loaded_image = np.uint16(imread(outfile)) + print(imgarr.shape, loaded_image.shape) + np.testing.assert_equal(imgarr.shape, loaded_image.shape[:-1]) diff --git a/browser/models.py b/browser/models.py new file mode 100644 index 000000000..96b61707e --- /dev/null +++ b/browser/models.py @@ -0,0 +1,85 @@ +"""SQL Alchemy database models.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging +import pickle +import timeit + +from flask_sqlalchemy import SQLAlchemy + + +logger = logging.getLogger('models.Project') # pylint: disable=C0103 +db = SQLAlchemy() # pylint: disable=C0103 + + +class Project(db.Model): + """Project table definition.""" + # pylint: disable=E1101 + __tablename__ = 'projects' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + filename = db.Column(db.Text, nullable=False) + state = db.Column(db.LargeBinary(length=(2 ** 32) - 1)) + subfolders = db.Column(db.Text, nullable=False) + createdAt = db.Column(db.TIMESTAMP, nullable=False, default=db.func.now()) + updatedAt = db.Column(db.TIMESTAMP, nullable=False, default=db.func.now(), + onupdate=db.func.current_timestamp()) + finished = db.Column(db.TIMESTAMP) + numUpdates = db.Column(db.Integer, nullable=False, default=0) + firstUpdate = db.Column(db.TIMESTAMP) + lastUpdate = db.Column(db.TIMESTAMP) + + def __init__(self, filename, state, subfolders): + self.filename = filename + self.state = state + self.subfolders = subfolders + + @staticmethod + def get_project_by_id(project_id): + """Return the project with the given ID, if it exists.""" + start = timeit.default_timer() + project = Project.query.filter_by(id=project_id).first() + logger.debug('Got project with ID = "%s" in %ss.', + project_id, timeit.default_timer() - start) + return project + + @staticmethod + def create_project(filename, state, subfolders): + """Create a new project.""" + start = timeit.default_timer() + state_data = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) + new_project = Project( + filename=filename, + state=state_data, + subfolders=subfolders) + db.session.add(new_project) + db.session.commit() + logger.debug('Created new project with ID = "%s" in %ss.', + new_project.id, timeit.default_timer() - start) + return new_project + + @staticmethod + def update_project(project, state): + """Update a project's current state.""" + start = timeit.default_timer() + if not project.firstUpdate: + project.firstUpdate = db.func.current_timestamp() + + project.state = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) + project.numUpdates += 1 + + db.session.commit() + logger.debug('Updated project with ID = "%s" in %ss.', + project.id, timeit.default_timer() - start) + + @staticmethod + def finish_project(project): + """Complete a project and set the state to null.""" + start = timeit.default_timer() + project.lastUpdate = project.updatedAt + project.finished = db.func.current_timestamp() + project.state = None + db.session.commit() # commit the changes + logger.debug('Finished project with ID = "%s" in %ss.', + project.id, timeit.default_timer() - start) diff --git a/browser/models_test.py b/browser/models_test.py new file mode 100644 index 000000000..323d9226a --- /dev/null +++ b/browser/models_test.py @@ -0,0 +1,49 @@ +"""Test for Caliban Models""" + +import pickle + +import pytest + +# from flask_sqlalchemy import SQLAlchemy + +import models + + +def test_project(db_session): + # TODO: is there a good way to separate these tests into unit tests? + + # test that no projects exist + project = models.Project.get_project_by_id(1) + assert project is None + + # test create project + filename = 'filename' + state = b'state_data' + subfolders = 'subfolders' + + project = models.Project.create_project( + filename=filename, + state=state, + subfolders=subfolders) + + valid_id = models.Project.id + + # test that the project can be found and is the same as the created one + found_project = models.Project.get_project_by_id(valid_id) + assert found_project == project + + # test project is updated + new_state = b'updated state data' + models.Project.update_project(project, new_state) + + # get the updated project and make sure the data is updated. + found_project = models.Project.get_project_by_id(valid_id) + pickled_state = pickle.dumps(new_state, pickle.HIGHEST_PROTOCOL) + assert found_project.state == pickled_state + + # test project is finished + models.Project.finish_project(project) + found_project = models.Project.get_project_by_id(valid_id) + assert found_project.state is None + assert found_project.finished is not None + assert found_project.lastUpdate is not None diff --git a/browser/requirements-test.txt b/browser/requirements-test.txt new file mode 100644 index 000000000..9cd005a5c --- /dev/null +++ b/browser/requirements-test.txt @@ -0,0 +1,9 @@ +pytest>=5.2 +pytest-cov==2.5.1 +pytest-mock +pytest-pep8 +pytest-flask +pytest-flask-sqlalchemy +fakeredis +six>=1.12 +coveralls diff --git a/browser/requirements.txt b/browser/requirements.txt index eb2e5ec02..bd0f67362 100644 --- a/browser/requirements.txt +++ b/browser/requirements.txt @@ -1,31 +1,11 @@ -awscli==1.16.259 -boto3==1.9.182 -botocore==1.12.182 -Click==7.0 -config==0.4.2 -cycler==0.10.0 -docutils==0.14 -Flask==1.0.3 -get==2019.4.13 -itsdangerous==1.1.0 -Jinja2==2.10.1 -jmespath==0.9.4 -jsonify==0.5 -kiwisolver==1.1.0 -MarkupSafe==1.1.1 -matplotlib==3.1.0 +flask==1.1.1 +flask-cors==3.0.8 +flask-sqlalchemy==2.4.1 +matplotlib==3.0.3 mysqlclient==1.4.5 -numpy==1.16.4 -pillow>=6.2.0 -post==2019.4.13 -public==2019.4.13 -pyparsing==2.4.0 -python-dateutil==2.8.0 +numpy>=1.16.4 +boto3==1.9.182 +scikit-image>=0.15.0,<0.17.0 python-decouple==3.1 -query-string==2019.4.13 -request==2019.4.13 -s3transfer==0.2.1 -six==1.12.0 -scikit-image==0.15.0 -urllib3==1.25.3 -Werkzeug==0.15.4 +pillow>=7.1.0 +flask-compress==1.5.0 diff --git a/browser/static/css/footer.css b/browser/static/css/footer.css deleted file mode 100644 index f27d3c25b..000000000 --- a/browser/static/css/footer.css +++ /dev/null @@ -1,3 +0,0 @@ -.footer-text { - text-align: center; -} \ No newline at end of file diff --git a/browser/static/css/form.css b/browser/static/css/form.css deleted file mode 100644 index e9b4c1d5e..000000000 --- a/browser/static/css/form.css +++ /dev/null @@ -1,54 +0,0 @@ -#say { -} - -.say-label { - text-align: center; -} - -#caliban-form { - width: 30em; - margin-top: 3em; - text-align: center; - padding: 3em 5em 5em 5em; -} - -.file-name-input { - display: flex; - box-sizing: border-box; - flex-shrink: 0; - flex-direction: column; - align-items: center; -} - -.file-name-input > :not(:first-child){ - margin-top: 2em; -} - -/* selects all children of the caliban-form except the first child. */ -#caliban-form > :not(:first-child) { - margin-top: 2em; -} - -.say-label, #say { - display: block; - vertical-align: top; -} - -#container { - display: flex; - box-sizing: border-box; - min-height: 93vh; - flex-direction: column; - align-items: center; -} - -.call-action { - font-size: 1.3125rem; - color: rgba(0, 0, 0, 0.54); - font-weight: 500; - line-height: 1.16667em; -} - -.form-help { - text-align: center; -} \ No newline at end of file diff --git a/browser/static/css/infopane.css b/browser/static/css/infopane.css deleted file mode 100644 index 4f52137ba..000000000 --- a/browser/static/css/infopane.css +++ /dev/null @@ -1,23 +0,0 @@ -.accordion { - background-color: #eee; - color: #444; - cursor: pointer; - padding: 18px; - width: 100%; - border: none; - text-align: left; - outline: none; - font-size: 15px; - transition: 0.4s; - } - - .active, .accordion:hover { - background-color: #ccc; - } - - .panel { - padding: 0 18px; - display: none; - background-color: white; - overflow: hidden; - } \ No newline at end of file diff --git a/browser/static/css/main.css b/browser/static/css/main.css index e179e800c..4d3dfd6d8 100644 --- a/browser/static/css/main.css +++ b/browser/static/css/main.css @@ -1,98 +1,99 @@ -.frame-info { - padding: 1em; +/* START Materialize Sticky Footer */ +body { + display: flex; + flex-direction: column; + min-height: 100vh; } -.file-input { - padding: 1em; +main { + flex: 1 0 auto; } +/* END Materialize Sticky Footer */ -.frame-info * { - display: block; +/* START container width override */ +.container { + margin: 0 auto; + max-width: 1280px; + width: 90%; } - -.view_mode * { - display: block; +@media only screen and (min-width: 601px) { + .container { + width: 90%; + } } - -.cell-info-block { - margin: 1em 1em 1em 0em; - padding: 1em; - border-radius: 1em; - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); +@media only screen and (min-width: 993px) { + .container { + width: 90%; + } } - -.image-state { - padding: 1em; +@media only screen and (min-width: 1201px) { + .container { + width: 90%; + } } +/* END container width override */ -.cell-info-block * { - display: block; +/* START navbar CSS */ +.nav-wrapper { + padding-left: 1em; + padding-right: 1em; } +/* END navbar CSS */ -#container { - margin: 0 auto; - margin-top: 100px; - display: flex; - box-sizing: border-box; - flex-shrink: 0; - flex-direction: row; - justify-content: center; - align-items: center; +/* START infopane CSS */ +.infopane { + border: 0; + margin-top: 0; } -#maintable { - width: 900px; - height: 500px; - /* outline: 1px dashed blue; */ +.infopane .active div.collapsible-header, +.infopane li div.collapsible-header:hover { + background-color: #ccc!important; + transition: 0.4s } -.changed-label { - text-align: center; +.infopane li .collapsible-body { + padding-top: 0; } - #logcol, #canvascol{ - vertical-align: top; - } +/* END infopane CSS */ -#logcol { - box-sizing: border-box; - border-radius: 1em; - width: 20em; +/* START log table CSS */ +.info-table tr td { + padding-bottom: 0.5em; + padding-top: 0.5em; + text-align: left; +} +.info-table tr td:nth-child(odd) { + width: 40%; } +.info-table tr td:nth-child(even) { + width: 60%; +} +/* END log table CSS */ #edit_brush_row, #edit_label_row, #edit_erase_row { - visibility: hidden; + visibility: hidden; } #frames, #slices { - width: 180px; - word-wrap: break-word; /*All browsers since IE 5.5+*/ - overflow-wrap: break-word; - max-height: 68px; - overflow: auto; + overflow: auto; + overflow-wrap: break-word; + word-wrap: break-word; /*All browsers since IE 5.5+*/ } #mode { - width: 180px; - word-wrap: break-word; - overflow-wrap: break-word; + overflow-wrap: break-word; + word-wrap: break-word; } #canvas { - cursor: crosshair; - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} - -#hidden_canvas, #hidden_seg_canvas { - display: none; + background: black; + cursor: crosshair; } -#canvascol { - vertical-align: top; +#output { + display: block; + max-width: 100%; + overflow-wrap: break-word; + word-wrap: break-word; } - -#give_filename { - display: block; - word-wrap: break-word; - overflow-wrap: break-word; - max-width: 300px; -} \ No newline at end of file diff --git a/browser/static/css/materialize.min.css b/browser/static/css/materialize.min.css new file mode 100644 index 000000000..74b1741b6 --- /dev/null +++ b/browser/static/css/materialize.min.css @@ -0,0 +1,13 @@ +/*! + * Materialize v1.0.0 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{-webkit-box-shadow:none !important;box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-small,.btn-floating,.dropdown-content,.collapsible,.sidenav{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-small:hover,.btn-floating:hover{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2);box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{-webkit-box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3);box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{-webkit-box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2);box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{-webkit-box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2);box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{-webkit-box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2);box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s}.hoverable:hover{-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width: 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;-webkit-transition:width .3s linear;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;-webkit-box-sizing:border-box;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:'liga';-moz-font-feature-settings:'liga';font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width: 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{-webkit-transition:background-color .3s;transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{-webkit-transition:background-color .3s;transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;-webkit-box-shadow:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);-webkit-transition:color .3s;transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{-webkit-transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .2s !important;transition:-webkit-transform .2s !important;transition:transform .2s !important;transition:transform .2s, -webkit-transform .2s !important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;-webkit-transition:color .3s ease;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease, background-color .28s ease;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width: 992px){.tabs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0%;transform-origin:50% 0%;visibility:hidden}.btn,.btn-large,.btn-small,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.disabled.btn-small,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-large:disabled,.btn-small:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-large[disabled],.btn-small[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;-webkit-box-shadow:none;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.disabled.btn-large:hover,.disabled.btn-small:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-large,.btn-small,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-large i,.btn-small i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-large:focus,.btn-small:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover,.btn-small:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;-webkit-transition:background-color .3s;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;-webkit-transition:none;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;-webkit-box-shadow:none;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;-webkit-transition:background-color .2s;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{-webkit-box-shadow:none;box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b2b2 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}.dropdown-trigger{cursor:pointer}/*! + * Waves v0.6.0 + * http://fian.my.id/Waves + * + * Copyright 2014 Alfiana E. Sibuea and other contributors + * Released under the MIT license + * https://github.com/fians/Waves/blob/master/LICENSE + */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{-webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;-webkit-box-shadow:none;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;-webkit-transition:all .3s;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix ~ .chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty ~ label{font-size:0.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;-webkit-transition:opacity .4s;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-transition:border .3s, -webkit-box-shadow .3s;transition:border .3s, -webkit-box-shadow .3s;transition:box-shadow .3s, border .3s;transition:box-shadow .3s, border .3s, -webkit-box-shadow .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid ~ label,input[type=text]:not(.browser-default):focus.valid ~ label,input[type=password]:not(.browser-default):focus.valid ~ label,input[type=email]:not(.browser-default):focus.valid ~ label,input[type=url]:not(.browser-default):focus.valid ~ label,input[type=time]:not(.browser-default):focus.valid ~ label,input[type=date]:not(.browser-default):focus.valid ~ label,input[type=datetime]:not(.browser-default):focus.valid ~ label,input[type=datetime-local]:not(.browser-default):focus.valid ~ label,input[type=tel]:not(.browser-default):focus.valid ~ label,input[type=number]:not(.browser-default):focus.valid ~ label,input[type=search]:not(.browser-default):focus.valid ~ label,textarea.materialize-textarea:focus.valid ~ label{color:#4CAF50}input:not([type]):focus.invalid ~ label,input[type=text]:not(.browser-default):focus.invalid ~ label,input[type=password]:not(.browser-default):focus.invalid ~ label,input[type=email]:not(.browser-default):focus.invalid ~ label,input[type=url]:not(.browser-default):focus.invalid ~ label,input[type=time]:not(.browser-default):focus.invalid ~ label,input[type=date]:not(.browser-default):focus.invalid ~ label,input[type=datetime]:not(.browser-default):focus.invalid ~ label,input[type=datetime-local]:not(.browser-default):focus.invalid ~ label,input[type=tel]:not(.browser-default):focus.invalid ~ label,input[type=number]:not(.browser-default):focus.invalid ~ label,input[type=search]:not(.browser-default):focus.invalid ~ label,textarea.materialize-textarea:focus.invalid ~ label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}input.valid:not([type]),input.valid:not([type]):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus,.select-wrapper.valid>input.select-dropdown{border-bottom:1px solid #4CAF50;-webkit-box-shadow:0 1px 0 0 #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input.invalid:not([type]),input.invalid:not([type]):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus,.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus{border-bottom:1px solid #F44336;-webkit-box-shadow:0 1px 0 0 #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).valid ~ .helper-text[data-success],input:not([type]):focus.valid ~ .helper-text[data-success],input:not([type]).invalid ~ .helper-text[data-error],input:not([type]):focus.invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default).valid ~ .helper-text[data-success],input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default).valid ~ .helper-text[data-success],input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default).valid ~ .helper-text[data-success],input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default).valid ~ .helper-text[data-success],input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default).valid ~ .helper-text[data-success],input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default).valid ~ .helper-text[data-success],input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default).valid ~ .helper-text[data-success],input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default).valid ~ .helper-text[data-success],input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error],textarea.materialize-textarea.valid ~ .helper-text[data-success],textarea.materialize-textarea:focus.valid ~ .helper-text[data-success],textarea.materialize-textarea.invalid ~ .helper-text[data-error],textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error],.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid ~ .helper-text[data-error]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}input:not([type]).valid ~ .helper-text:after,input:not([type]):focus.valid ~ .helper-text:after,input[type=text]:not(.browser-default).valid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=password]:not(.browser-default).valid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=email]:not(.browser-default).valid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=url]:not(.browser-default).valid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=time]:not(.browser-default).valid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=date]:not(.browser-default).valid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=tel]:not(.browser-default).valid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=number]:not(.browser-default).valid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=search]:not(.browser-default).valid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after,textarea.materialize-textarea.valid ~ .helper-text:after,textarea.materialize-textarea:focus.valid ~ .helper-text:after,.select-wrapper.valid ~ .helper-text:after{content:attr(data-success);color:#4CAF50}input:not([type]).invalid ~ .helper-text:after,input:not([type]):focus.invalid ~ .helper-text:after,input[type=text]:not(.browser-default).invalid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=password]:not(.browser-default).invalid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=email]:not(.browser-default).invalid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=url]:not(.browser-default).invalid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=time]:not(.browser-default).invalid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=date]:not(.browser-default).invalid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=tel]:not(.browser-default).invalid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=number]:not(.browser-default).invalid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=search]:not(.browser-default).invalid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after,textarea.materialize-textarea.invalid ~ .helper-text:after,textarea.materialize-textarea:focus.invalid ~ .helper-text:after,.select-wrapper.invalid ~ .helper-text:after{content:attr(data-error);color:#F44336}input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after,.select-wrapper+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;-webkit-transition:.2s opacity ease-out, .2s color ease-out;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix ~ label,.input-field.col .prefix ~ .validate ~ label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;-webkit-transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:transform .2s ease-out, color .2s ease-out;transition:transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out;-webkit-transform-origin:0% 100%;transform-origin:0% 100%;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;-webkit-transition:color .2s;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea,.input-field .prefix ~ label,.input-field .prefix ~ .validate ~ label,.input-field .prefix ~ .helper-text,.input-field .prefix ~ .autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;-webkit-transition:.3s background-color;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;-webkit-box-shadow:none;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;-webkit-box-shadow:none;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;-webkit-transition:.3s color;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;-webkit-box-sizing:border-box;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-transition:.28s ease;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;-webkit-transition:.28s ease;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{-webkit-transform:scale(0.5);transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;-webkit-transition:.2s;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;-webkit-transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 10px rgba(38,166,154,0.26);box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.sidenav.sidenav-fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0% 50%;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:rgba(255,255,255,0.5);-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;-webkit-transition:visibility 0s .3s;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;-webkit-transition:visibility 0s;transition:visibility 0s}.tap-target-wrapper.open .tap-target{-webkit-transform:scale(1);transform:scale(1);opacity:.95;-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;-webkit-transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s 1s;transition:opacity .3s, transform .3s, visibility 0s 1s, -webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;-webkit-box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s, -webkit-transform .3s}.tap-target-wave::after{visibility:hidden;-webkit-transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s;transition:opacity .3s, transform .3s, visibility 0s, -webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;-webkit-transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, transform .3s;transition:opacity .3s, transform .3s, -webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.datepicker-controls{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width: 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.datepicker-date-display{-webkit-box-flex:0;-webkit-flex:0 1 270px;-ms-flex:0 1 270px;flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{-webkit-transition:opacity 350ms, -webkit-transform 350ms;transition:opacity 350ms, -webkit-transform 350ms;transition:transform 350ms, opacity 350ms;transition:transform 350ms, opacity 350ms, -webkit-transform 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1, 1.1);transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(0.8, 0.8);transform:scale(0.8, 0.8)}.timepicker-canvas{-webkit-transition:opacity 175ms;transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width: 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}} diff --git a/browser/static/css/navigation.css b/browser/static/css/navigation.css deleted file mode 100644 index 933bccf31..000000000 --- a/browser/static/css/navigation.css +++ /dev/null @@ -1,45 +0,0 @@ - -.navbar { - /* Font */ - font-weight: 500; - font-family: "Roboto", "Helvetica", "Arial", sans-serif; - /* */ - color: #ffffff; - background-color: #3f51b5; - width: 100%; - display: flex; - z-index: 1100; - box-sizing: border-box; - flex-shrink: 0; - flex-direction: row; - height: 5em; - justify-content: space-between; - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} - -.logo, .nav-link { - width: 100px; -} - -.logo { - font-size: 1.5em; - margin-top: 1em; - margin-left: 1em; - order: 1; - align-content: flex-start; -} - -.nav-link { -} - -.buttons { - float: right; - order: 2; - margin-top: 2em; - align-content: flex-end; - margin-right: 1em; -} - -.nav-link:last-child{ - margin-left: 1em; -} \ No newline at end of file diff --git a/browser/static/css/normalize.css b/browser/static/css/normalize.css index fb89b52b9..192eb9ce4 100644 --- a/browser/static/css/normalize.css +++ b/browser/static/css/normalize.css @@ -1,12 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + body { - margin: 0; - font-family: "Roboto", "Helvetica", "Arial", sans-serif; - /* font */ - color: rgba(0, 0, 0, 0.54); - font-weight: 500; - line-height: 1.16667em; - /*text-transform: uppercase;*/ - /* */ - color: rgba(0, 0, 0, 0.54); - background-color: white; -} \ No newline at end of file + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/browser/static/favicon.ico b/browser/static/favicon.ico new file mode 100644 index 000000000..93ca78bfd Binary files /dev/null and b/browser/static/favicon.ico differ diff --git a/browser/static/js/adjust.js b/browser/static/js/adjust.js new file mode 100644 index 000000000..a6f31275b --- /dev/null +++ b/browser/static/js/adjust.js @@ -0,0 +1,394 @@ +// helper functions + +class ImageAdjuster { + constructor(width, height, rgb, channelMax) { + // canvas element used for image processing + this.canvas = document.createElement('canvas'); + this.canvas.id = 'adjustCanvas'; + + // this canvas should never be seen + this.canvas.style.display = 'none'; + + // same dimensions as true image size + this.canvas.height = height; + this.canvas.width = width; + document.body.appendChild(this.canvas); + + this.ctx = this.canvas.getContext('2d'); + this.ctx.imageSmoothingEnabled = false; + + // these will never change once initialized + this.height = height; + this.width = width; + + this.rgb = rgb; + + // this can be between 0.0 and 1.0, inclusive (1 is fully opaque) + // want to make user-adjustable in future + this.labelTransparency = 0.3; + + // brightness and contrast adjustment + this._minContrast = -100; + this._maxContrast = 700; + + this._minBrightness = -512; + this._maxBrightness = 255; + + // image adjustments are stored per channel for better viewing + this.contrastMap = new Map(); + this.brightnessMap = new Map(); + this.invertMap = new Map(); + + for (let i = 0; i < channelMax; i++) { + this.brightnessMap.set(i, 0); + this.contrastMap.set(i, 0); + this.invertMap.set(i, true); + } + this.brightness = this.brightnessMap.get(0); + this.contrast = this.contrastMap.get(0); + this.displayInvert = this.invertMap.get(0); + + // raw and adjusted image storage + // cascasding image updates if raw or seg is reloaded + this.rawImage = new Image(); + this.contrastedRaw = new Image(); + this.preCompRaw = new Image(); + + this.segImage = new Image(); + this.preCompSeg = new Image(); + + // adjusted raw + annotations + this.compositedImg = new Image(); + + // composite image + outlines, transparent highlight + this.postCompImg = new Image(); + + // TODO: these still rely on main js global variables, revisit in future + if (rgb) { + this.rawImage.onload = () => this.contrastRaw(); + this.contrastedRaw.onload = () => this.rawAdjust(seg_array, current_highlight, edit_mode, brush, mode); + this.segImage.onload = () => this.preCompAdjust(seg_array, current_highlight, edit_mode, brush, mode); + this.preCompSeg.onload = () => this.segAdjust(seg_array, current_highlight, edit_mode, brush, mode); + } else { + this.rawImage.onload = () => this.contrastRaw(); + this.contrastedRaw.onload = () => this.preCompRawAdjust(); + this.preCompRaw.onload = () => this.rawAdjust(seg_array, current_highlight, edit_mode, brush, mode); + this.segImage.onload = () => this.preCompAdjust(seg_array, current_highlight, edit_mode, brush, mode); + this.preCompSeg.onload = () => this.segAdjust(seg_array, current_highlight, edit_mode, brush, mode); + this.compositedImg.onload = () => this.postCompAdjust(seg_array, edit_mode, brush); + } + + this.rawLoaded = false; + this.segLoaded = false; + } + + // getters for brightness/contrast allowed ranges + // no setters; these should remain fixed + get minBrightness() { + return this._minBrightness; + } + + get maxBrightness() { + return this._maxBrightness; + } + + get minContrast() { + return this._minContrast; + } + + get maxContrast() { + return this._maxContrast; + } + + changeContrast(inputChange) { + const modContrast = -Math.sign(inputChange) * 4; + // stop if fully desaturated + let newContrast = Math.max(this.contrast + modContrast, this.minContrast); + // stop at 8x contrast + newContrast = Math.min(newContrast, this.maxContrast); + + if (newContrast !== this.contrast) { + // need to retrigger downstream image adjustments + this.rawLoaded = false; + this.contrast = newContrast; + this.contrastRaw(); + } + } + + changeBrightness(inputChange) { + const modBrightness = -Math.sign(inputChange); + // limit how dim image can go + let newBrightness = Math.max(this.brightness + modBrightness, this.minBrightness); + // limit how bright image can go + newBrightness = Math.min(newBrightness, this.maxBrightness); + + if (newBrightness !== this.brightness) { + this.rawLoaded = false; + this.brightness = newBrightness; + this.contrastRaw(); + } + } + + resetBrightnessContrast() { + this.brightness = 0; + this.contrast = 0; + this.rawLoaded = false; + this.contrastRaw(); + } + + toggleInvert() { + this.displayInvert = !this.displayInvert; + this.preCompRawAdjust(); + } + + // modify image data in place to recolor + _recolorScaled(data, i, j, jlen, r = 255, g = 255, b = 255) { + // location in 1D array based on i and j + const pixelNum = (jlen * j + i) * 4; + // set to color by changing RGB values + // data is clamped 8bit type, so +255 sets to 255, -255 sets to 0 + data[pixelNum] += r; + data[pixelNum + 1] += g; + data[pixelNum + 2] += b; + } + + // image adjustment functions: take img as input and manipulate data attribute + // pixel data is 1D array of 8bit RGBA values + _contrastImage(img, contrast = 0, brightness = 0) { + const d = img.data; + contrast = (contrast / 100) + 1; + for (let i = 0; i < d.length; i += 4) { + d[i] = d[i] * contrast + brightness; + d[i + 1] = d[i + 1] * contrast + brightness; + d[i + 2] = d[i + 2] * contrast + brightness; + } + } + + _grayscale(img) { + const data = img.data; + for (let i = 0; i < data.length; i += 4) { + const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + data[i] = avg; // red + data[i + 1] = avg; // green + data[i + 2] = avg; // blue + } + } + + _invert(img) { + const data = img.data; + for (let i = 0; i < data.length; i += 4) { + data[i] = 255 - data[i]; // red + data[i + 1] = 255 - data[i + 1]; // green + data[i + 2] = 255 - data[i + 2]; // blue + } + } + + preCompositeLabelMod(img, segArray, h1, h2) { + let r, g, b; + const ann = img.data; + // use label array to figure out which pixels to recolor + for (let j = 0; j < segArray.length; j += 1) { // y + for (let i = 0; i < segArray[j].length; i += 1) { // x + const jlen = segArray[j].length; + const currentVal = Math.abs(segArray[j][i]); + if (currentVal === h1 || currentVal === h2) { + this._recolorScaled(ann, i, j, jlen, r = 255, g = -255, b = -255); + } + } + } + } + + postCompositeLabelMod(img, segArray, + redOutline = false, r1 = -1, + singleOutline = false, o1 = -1, + outlineAll = false, + translucent = false, t1 = -1, t2 = -1) { + let r, g, b; + const ann = img.data; + // use label array to figure out which pixels to recolor + for (let j = 0; j < segArray.length; j += 1) { // y + for (let i = 0; i < segArray[j].length; i += 1) { // x + const jlen = segArray[j].length; + const currentVal = segArray[j][i]; + // outline red + if (redOutline && currentVal === -r1) { + this._recolorScaled(ann, i, j, jlen, r = 255, g = -255, b = -255); + continue; + // outline white single + } else if (singleOutline && currentVal === -o1) { + this._recolorScaled(ann, i, j, jlen, r = 255, g = 255, b = 255); + continue; + // outline all remaining edges with white + } else if (outlineAll && currentVal < 0) { + this._recolorScaled(ann, i, j, jlen, r = 255, g = 255, b = 255); + continue; + // translucent highlight + } else if (translucent && + (Math.abs(currentVal) === t1 || Math.abs(currentVal) === t2)) { + this._recolorScaled(ann, i, j, jlen, r = 60, g = 60, b = 60); + continue; + } + } + } + } + + // apply contrast+brightness to raw image + contrastRaw() { + // draw rawImage so we can extract image data + this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.drawImage(this.rawImage, 0, 0, this.width, this.height); + const rawData = this.ctx.getImageData(0, 0, this.width, this.height); + this._contrastImage(rawData, this.contrast, this.brightness); + this.ctx.putImageData(rawData, 0, 0); + + this.contrastedRaw.src = this.canvas.toDataURL(); + } + + preCompAdjust(segArray, currentHighlight, editMode, brush, mode) { + this.segLoaded = false; + + // draw segImage so we can extract image data + this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.drawImage(this.segImage, 0, 0, this.width, this.height); + + if (currentHighlight) { + const segData = this.ctx.getImageData(0, 0, this.width, this.height); + let h1, h2; + + if (editMode) { + h1 = brush.value; + h2 = -1; + } else { + h1 = mode.highlighted_cell_one; + h2 = mode.highlighted_cell_two; + } + + // highlight + this.preCompositeLabelMod(segData, segArray, h1, h2); + this.ctx.putImageData(segData, 0, 0); + } + + // once this new src is loaded, displayed image will be rerendered + this.preCompSeg.src = this.canvas.toDataURL(); + } + + // adjust raw further, pre-compositing (use to draw when labels hidden) + preCompRawAdjust() { + // further adjust raw image + this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.drawImage(this.contrastedRaw, 0, 0, this.width, this.height); + const rawData = this.ctx.getImageData(0, 0, this.width, this.height); + this._grayscale(rawData); + if (this.displayInvert) { + this._invert(rawData); + } + this.ctx.putImageData(rawData, 0, 0); + + this.preCompRaw.src = this.canvas.toDataURL(); + } + + // composite annotations on top of adjusted raw image + compositeImages() { + this.ctx.drawImage(this.preCompRaw, 0, 0, this.width, this.height); + + // add labels on top + this.ctx.save(); + this.ctx.globalAlpha = this.labelTransparency; + this.ctx.drawImage(this.preCompSeg, 0, 0, this.width, this.height); + this.ctx.restore(); + + this.compositedImg.src = this.canvas.toDataURL(); + } + + // apply outlines, transparent highlighting + postCompAdjust(segArray, editMode, brush) { + // draw compositedImg so we can extract image data + this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.drawImage(this.compositedImg, 0, 0, this.width, this.height); + + // add outlines around conversion brush target/value + const imgData = this.ctx.getImageData(0, 0, this.width, this.height); + + let redOutline, r1, singleOutline, o1, outlineAll, translucent, t1, t2; + // red outline for conversion brush target + if (editMode && brush.conv && brush.target !== -1) { + redOutline = true; + r1 = brush.target; + } + if (editMode && brush.conv && brush.value !== -1) { + singleOutline = true; + o1 = brush.value; + } + + this.postCompositeLabelMod( + imgData, segArray, redOutline, r1, singleOutline, o1, + outlineAll, translucent, t1, t2); + + this.ctx.putImageData(imgData, 0, 0); + + this.postCompImg.src = this.canvas.toDataURL(); + } + + // apply outlines, transparent highlighting for RGB + postCompAdjustRGB(segArray, currentHighlight, editMode, brush, mode) { + // draw contrastedRaw so we can extract image data + this.ctx.clearRect(0, 0, this.width, this.height); + this.ctx.drawImage(this.contrastedRaw, 0, 0, this.width, this.height); + + // add outlines around conversion brush target/value + const imgData = this.ctx.getImageData(0, 0, this.width, this.height); + + let redOutline, r1, singleOutline, o1, translucent, t1, t2; + + // red outline for conversion brush target + if (editMode && brush.conv && brush.target !== -1) { + redOutline = true; + r1 = brush.target; + } + + // singleOutline never on for RGB + + const outlineAll = true; + + // translucent highlight + if (currentHighlight) { + translucent = true; + if (editMode) { + t1 = brush.value; + } else { + t1 = mode.highlighted_cell_one; + t2 = mode.highlighted_cell_two; + } + } + + this.postCompositeLabelMod( + imgData, segArray, redOutline, r1, singleOutline, o1, + outlineAll, translucent, t1, t2); + + this.ctx.putImageData(imgData, 0, 0); + + this.postCompImg.src = this.canvas.toDataURL(); + } + + segAdjust(segArray, currentHighlight, editMode, brush, mode) { + this.segLoaded = true; + if (this.rawLoaded && this.segLoaded) { + if (this.rgb) { + this.postCompAdjustRGB(segArray, currentHighlight, editMode, brush, mode); + } else { + this.compositeImages(); + } + } + } + + rawAdjust(segArray, currentHighlight, editMode, brush, mode) { + this.rawLoaded = true; + if (this.rawLoaded && this.segLoaded) { + if (this.rgb) { + this.postCompAdjustRGB(segArray, currentHighlight, editMode, brush, mode); + } else { + this.compositeImages(); + } + } + } +} diff --git a/browser/static/js/brush.js b/browser/static/js/brush.js new file mode 100644 index 000000000..d46323e3a --- /dev/null +++ b/browser/static/js/brush.js @@ -0,0 +1,262 @@ +class Brush { + constructor(scale, height, width, pad) { + // center of brush (scaled) + this.x = 0; + this.y = 0; + // size of brush in pixels + this._size = 5; + + // status of eraser + this._erase = false; + + // normal brush attributes + this._regTarget = 0; + this._regValue = 1; + + // conversion brush attributes + this._convTarget = -1; + this._convValue = -1; + + // status of conversion brush mode + this._conv = false; + + // threshold/box attributes + this.show = true; // showing brush shape + // -2*pad will always be out of range for annotators + // anchored corner of bounding box + this._threshX = -2 * pad; + this._threshY = -2 * pad; + this._showBox = false; + + // how to draw brush/box shadow + this._outlineColor = 'white'; + // opacity only applies to interior + this._fillColor = 'white'; + this._opacity = 0.3; + + // attributes needed to match visible canvas + this._height = height; + this._width = width; + this._padding = pad; + + // create hidden canvas to store brush preview + this.canvas = document.createElement('canvas'); + this.canvas.id = 'brushCanvas'; + // this canvas should never be seen + this.canvas.style.display = 'none'; + this.canvas.height = height; + this.canvas.width = width; + document.body.appendChild(this.canvas); + this.ctx = document.getElementById('brushCanvas').getContext('2d'); + // set fillStyle here, it will never change + this.ctx.fillStyle = this._fillColor; + } + + get size() { + return this._size; + } + + // set bounds on size of brush, update brushview appropriately + set size(newSize) { + // don't need brush to take up whole frame + if (newSize > 0 && newSize < this._height / 2 && + newSize < this._width / 2 && newSize !== this._size) { + // size is size in pixels, used to modify source array + this._size = newSize; + // update brush preview with new size + this.refreshView(); + } + } + + get erase() { + return this._erase; + } + + set erase(bool) { + // eraser is either true or false + if (typeof bool === 'boolean') { + this._erase = bool; + // red outline is visual indicator for eraser being on + if (this._erase) { + this._outlineColor = 'red'; + // white outline if eraser is off (drawing normally) + } else { + this._outlineColor = 'white'; + } + } + } + + // target = value of array that backend will overwrite + get target() { + if (this._conv) { + return this._convTarget; + } else { + // always 0 + return this._regTarget; + } + } + + // only conversion brush target can change + set target(val) { + // never set conversion brush to modify background + if (this._conv && val !== 0) { + this._convTarget = val; + } + } + + // value = label that gets added to annotation array in backend + get value() { + if (this._conv) { + return this._convValue; + } else { + return this._regValue; + } + } + + // can change brush's normal value or conversion brush value + set value(val) { + // never set conversion brush to modify background + // logic for val != target is elsewhere to prevent + // value picking from finishing early + if (this._conv && val !== 0) { + this._convValue = val; + // regular brush never has value less than 1 + } else if (!this._conv) { + this._regValue = Math.max(val, 1); + } + } + + // whether or not conversion brush is on + get conv() { + return this._conv; + } + + set conv(bool) { + this._conv = bool; + // if turning off conv brush, reset conv values + if (!bool) { + this._convValue = -1; + this._convTarget = -1; + } + // if conv brush is on, temporarily disable eraser, even if erase is true + if (this._erase && !this._conv) { + this._outlineColor = 'red'; + } else { + this._outlineColor = 'white'; + } + } + + get threshX() { + return this._threshX; + } + + set threshX(x) { + // clearing anchor corner + if (x === -2 * this._padding) { + this._threshX = x; + this._showBox = false; + this.clearView(); + // setting anchor corner + } else { + this._threshX = x; + this._showBox = true; + this.boxView(); + } + } + + get threshY() { + return this._threshY; + } + + set threshY(y) { + // clearing anchor corner + if (y === -2 * this._padding) { + this._threshY = y; + this._showBox = false; + this.clearView(); + // setting anchor corner + } else { + this._threshY = y; + this._showBox = true; + this.boxView(); + } + } + + // reset thresholding box anchor corner + clearThresh() { + this.threshX = -2 * this._padding; + this.threshY = -2 * this._padding; + // restore normal brush view + this.show = true; + this.addToView(); + } + + // clear ctx + clearView() { + this.ctx.clearRect(0, 0, this._width, this._height); + } + + // adds brush shadow to ctx + addToView() { + this.ctx.beginPath(); + this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, true); + this.ctx.closePath(); + // no opacity needed; just shows where brush has been + this.ctx.fill(); + } + + // clear previous view and update with current view + refreshView() { + this.clearView(); + this.addToView(); + } + + // display bounding box for thresholding + boxView() { + // clear previous box shape + this.clearView(); + // only if actively drawing box (anchor corner set) + if (this._showBox) { + // interior of box; will be added to visible canvas with opacity + this.ctx.fillRect( + this.threshX, this.threshY, + this.x - this.threshX, + this.y - this.threshY); + } + } + + // draw brush preview onto destination ctx + draw(ctxDst, sx, sy, swidth, sheight, mag) { + // draw translucent brush trace + ctxDst.save(); + ctxDst.globalAlpha = this._opacity; + ctxDst.globalCompositeOperation = 'source-over'; + const ctxDstHeight = ctxDst.canvas.height; + const ctxDstWidth = ctxDst.canvas.width; + ctxDst.drawImage( + this.canvas, sx, sy, swidth, sheight, + this._padding, this._padding, + ctxDstWidth - 2 * this._padding, + ctxDstHeight - 2 * this._padding); + ctxDst.restore(); + + // add solid outline around current brush location + if (this.show) { + ctxDst.beginPath(); + const cX = (this.x - sx) * mag + this._padding; + const cY = (this.y - sy) * mag + this._padding; + ctxDst.arc(cX, cY, mag * this.size, 0, Math.PI * 2, true); + ctxDst.strokeStyle = this._outlineColor; // either red or white + ctxDst.closePath(); + ctxDst.stroke(); + } else if (this._showBox) { + // draw box around threshold area + ctxDst.strokeStyle = 'white'; + const boxStartX = (this.threshX - sx) * mag + this._padding; + const boxStartY = (this.threshY - sy) * mag + this._padding; + const boxWidth = (this.x - this.threshX) * mag; + const boxHeight = (this.y - this.threshY) * mag; + ctxDst.strokeRect(boxStartX, boxStartY, boxWidth, boxHeight); + } + } +} diff --git a/browser/static/js/infopane.js b/browser/static/js/infopane.js deleted file mode 100644 index ee0667544..000000000 --- a/browser/static/js/infopane.js +++ /dev/null @@ -1,14 +0,0 @@ -var acc = document.getElementsByClassName("accordion"); -var i; - -for (i = 0; i < acc.length; i++) { - acc[i].addEventListener("click", function() { - this.classList.toggle("active"); - var panel = this.nextElementSibling; - if (panel.style.display === "block") { - panel.style.display = "none"; - } else { - panel.style.display = "block"; - } - }); -} \ No newline at end of file diff --git a/browser/static/js/main_track.js b/browser/static/js/main_track.js index 3cca93c84..bc74aa1fb 100644 --- a/browser/static/js/main_track.js +++ b/browser/static/js/main_track.js @@ -50,6 +50,11 @@ class Mode { // toggle rendering_raw rendering_raw = !rendering_raw; render_image_display(); + } else if (key === '0') { + // reset brightness adjustments + brightness = 0; + current_contrast = 0; + render_image_display(); } } @@ -61,44 +66,29 @@ class Mode { render_image_display(); } else if (key === "=") { // increase edit_value up to max label + 1 (guaranteed unused) - edit_value = Math.min(edit_value + 1, maxTrack + 1); + brush.value = Math.min(brush.value + 1, maxTrack + 1); render_info_display(); } else if (key === "-") { // decrease edit_value, minimum 1 - edit_value = Math.max(edit_value - 1, 1); + brush.value -= 1; render_info_display(); } else if (key === "x") { // turn eraser on and off - erase = !erase; - render_info_display(); + brush.erase = !brush.erase; + render_image_display(); } else if (key === "ArrowDown") { // decrease brush size, minimum size 1 - brush_size = Math.max(brush_size - 1, 1); - - // update brush object with its new size - let hidden_ctx = $('#hidden_canvas').get(0).getContext("2d"); - hidden_ctx.clearRect(0,0,dimensions[0],dimensions[1]); - brush.radius = brush_size * scale; - brush.draw(hidden_ctx); - + brush.size -= 1; // redraw the frame with the updated brush preview render_image_display(); } else if (key === "ArrowUp") { - // increase brush size, shouldn't be larger than the image - brush_size = Math.min(self.brush_size + 1, - dimensions[0]/scale, dimensions[1]/scale); - - // update brush with its new size - let hidden_ctx = $('#hidden_canvas').get(0).getContext("2d"); - hidden_ctx.clearRect(0,0,dimensions[0],dimensions[1]); - brush.radius = brush_size * scale; - brush.draw(hidden_ctx); - + // increase brush size, diameter shouldn't be larger than the image + brush.size += 1; // redraw the frame with the updated brush preview render_image_display(); } else if (key === 'n') { // set edit value to something unused - edit_value = maxTrack + 1; + brush.value = maxTrack + 1; render_info_display(); // when value of brush determines color of brush, render_image instead } else if (key === 'i') { @@ -137,8 +127,10 @@ class Mode { handle_mode_single_keybind(key) { if (key === "f") { // hole fill - this.info = { "label": this.info['label'], - "frame": current_frame}; + this.info = { + "label": this.info['label'], + "frame": current_frame + }; this.kind = Modes.question; this.action = "fill_hole"; this.prompt = "Select hole to fill in cell " + this.info['label']; @@ -152,7 +144,7 @@ class Mode { } else if (key === "x") { // delete label from frame this.kind = Modes.question; - this.action = "delete_cell"; + this.action = "delete_mask"; this.prompt = "delete label " + this.info.label + " in frame " + this.info.frame + "? " + answer; render_info_display(); } else if (key === "-") { @@ -214,31 +206,37 @@ class Mode { // keybinds that apply in bulk mode, answering question/prompt handle_mode_question_keybind(key) { if (key === " ") { - if (this.action === "flood_cell") { - action("flood_cell", this.info); + if (this.action === "flood_contiguous") { + action("flood_contiguous", this.info); } else if (this.action === "trim_pixels") { action("trim_pixels", this.info); } if (this.action === "new_track") { - action("create_all_new", this.info); - } else if (this.action === "delete_cell") { + action("new_track", this.info); + } else if (this.action === "delete_mask") { action(this.action, this.info); } else if (this.action === "set_parent") { if (this.info.label_1 !== this.info.label_2 && this.info.frame_1 < this.info.frame_2) { - let send_info = {"label_1": this.info.label_1, - "label_2": this.info.label_2}; + let send_info = { + "label_1": this.info.label_1, + "label_2": this.info.label_2 + }; action(this.action, send_info); } } else if (this.action === "replace") { if (this.info.label_1 !== this.info.label_2) { - let send_info = {"label_1": this.info.label_1, - "label_2": this.info.label_2}; + let send_info = { + "label_1": this.info.label_1, + "label_2": this.info.label_2 + }; action(this.action, send_info); } } else if (this.action === "swap_cells") { if (this.info.label_1 !== this.info.label_2) { - let send_info = {"label_1": this.info.label_1, - "label_2": this.info.label_2} + let send_info = { + "label_1": this.info.label_1, + "label_2": this.info.label_2 + }; action("swap_tracks", send_info); } } else if (this.action === "watershed") { @@ -256,13 +254,15 @@ class Mode { this.clear(); } else if (key === "s") { if (this.action === "new_track") { - action("create_single_new", this.info); + action("new_single_cell", this.info); } else if (this.action === "swap_cells") { if (this.info.label_1 !== this.info.label_2 && this.info.frame_1 === this.info.frame_2) { - let send_info = {"label_1": this.info.label_1, - "label_2": this.info.label_2, - "frame": this.info.frame_1}; + let send_info = { + "label_1": this.info.label_1, + "label_2": this.info.label_2, + "frame": this.info.frame_1 + }; action("swap_single_frame", send_info); } } @@ -290,11 +290,14 @@ class Mode { } handle_draw() { - action("handle_draw", { "trace": JSON.stringify(mouse_trace), - "edit_value": edit_value, - "brush_size": brush_size, - "erase": erase, - "frame": current_frame}); + action("handle_draw", { + "trace": JSON.stringify(mouse_trace), // stringify array so it doesn't get messed up + "target_value": brush.target, // value that we're overwriting + "brush_value": brush.value, // we don't update caliban with edit_value, etc each time they change + "brush_size": brush.size, // so we need to pass them in as args + "erase": (brush.erase && !brush.conv), + "frame": current_frame + }); mouse_trace = []; this.clear() } @@ -303,28 +306,34 @@ class Mode { if (evt.altKey) { // alt+click this.kind = Modes.question; - this.action = "flood_cell"; - this.info = {"label": current_label, - "frame": current_frame, - "x_location": mouse_x, - "y_location": mouse_y} + this.action = "flood_contiguous"; + this.info = { + "label": current_label, + "frame": current_frame, + "x_location": mouse_x, + "y_location": mouse_y + } this.prompt = "SPACE = FLOOD SELECTED CELL WITH NEW LABEL / ESC = CANCEL"; this.highlighted_cell_one = current_label; } else if (evt.shiftKey) { // shift+click this.kind = Modes.question; this.action = "trim_pixels"; - this.info = {"label": current_label, - "frame": current_frame, - "x_location": mouse_x, - "y_location": mouse_y}; + this.info = { + "label": current_label, + "frame": current_frame, + "x_location": mouse_x, + "y_location": mouse_y + }; this.prompt = "SPACE = TRIM DISCONTIGUOUS PIXELS FROM CELL / ESC = CANCEL"; this.highlighted_cell_one = current_label; } else { // normal click this.kind = Modes.single; - this.info = {"label": current_label, - "frame": current_frame}; + this.info = { + "label": current_label, + "frame": current_frame + }; this.highlighted_cell_one = current_label; this.highlighted_cell_two = -1; temp_x = mouse_x; @@ -334,10 +343,12 @@ class Mode { handle_mode_question_click(evt) { if (this.action === "fill_hole" && current_label === 0) { - this.info = { "label": this.info['label'], - "frame": current_frame, - "x_location": mouse_x, - "y_location": mouse_y }; + this.info = { + "label": this.info['label'], + "frame": current_frame, + "x_location": mouse_x, + "y_location": mouse_y + }; action(this.action, this.info); this.clear(); } @@ -349,28 +360,32 @@ class Mode { this.highlighted_cell_one = this.info.label; this.highlighted_cell_two = current_label; - this.info = {"label_1": this.info.label, - "label_2": current_label, - "frame_1": this.info.frame, - "frame_2": current_frame, - "x1_location": temp_x, - "y1_location": temp_y, - "x2_location": mouse_x, - "y2_location": mouse_y}; + this.info = { + "label_1": this.info.label, + "label_2": current_label, + "frame_1": this.info.frame, + "frame_2": current_frame, + "x1_location": temp_x, + "y1_location": temp_y, + "x2_location": mouse_x, + "y2_location": mouse_y + }; } handle_mode_multiple_click(evt) { this.highlighted_cell_one = this.info.label_1; this.highlighted_cell_two = current_label; - this.info = {"label_1": this.info.label_1, - "label_2": current_label, - "frame_1": this.info.frame_1, - "frame_2": current_frame, - "x1_location": temp_x, - "y1_location": temp_y, - "x2_location": mouse_x, - "y2_location": mouse_y}; + this.info = { + "label_1": this.info.label_1, + "label_2": current_label, + "frame_1": this.info.frame_1, + "frame_2": current_frame, + "x1_location": temp_x, + "y1_location": temp_y, + "x2_location": mouse_x, + "y2_location": mouse_y + }; } click(evt) { @@ -422,6 +437,7 @@ var temp_y = 0; var rendering_raw = false; let display_invert = true; var current_contrast = 0; +let brightness = 0; var current_frame = 0; var current_label = 0; var current_highlight = false; @@ -436,23 +452,20 @@ var seg_array; var scale; var mouse_x = 0; var mouse_y = 0; +const padding = 5; var edit_mode = false; -var edit_value = 1; -var brush_size = 1; -var erase = false; let mousedown = false; var answer = "(SPACE=YES / ESC=NO)"; var project_id = undefined; var brush; let mouse_trace = []; -function upload_file() { +function upload_file(cb) { $.ajax({ - type:'POST', - url:"upload_file/" + project_id, - success: function (payload) { - }, - async: false + type: 'POST', + url: `upload_file/${project_id}`, + success: cb, + async: true }); } @@ -461,11 +474,10 @@ function upload_file() { function contrast_image(img, contrast) { let d = img.data; contrast = (contrast / 100) + 1; - /* let intercept = 128 * (1 - contrast); */ for (let i = 0; i < d.length; i += 4) { - d[i] *= contrast; - d[i + 1] *= contrast; - d[i + 2] *= contrast; + d[i] = d[i]*contrast + brightness; + d[i + 1] = d[i+1]*contrast + brightness; + d[i + 2] = d[i+2]*contrast + brightness; } return img; } @@ -478,7 +490,7 @@ function highlight(img, label) { for (var i = 0; i < seg_array[j].length; i += 1){ //x let jlen = seg_array[j].length; - if (seg_array[j][i] === label){ + if (Math.abs(seg_array[j][i]) === label){ // fill in all pixels affected by scale // k and l get the pixels that are part of the original pixel that has been scaled up for (var k = 0; k < scale; k +=1) { @@ -524,7 +536,7 @@ function label_under_mouse() { let new_label; if (img_y >= 0 && img_y < seg_array.length && img_x >= 0 && img_x < seg_array[0].length) { - new_label = seg_array[img_y][img_x]; //check array value at mouse location + new_label = Math.abs(seg_array[img_y][img_x]); //check array value at mouse location } else { new_label = 0; } @@ -551,22 +563,22 @@ function render_highlight_info() { function render_edit_info() { if (edit_mode) { - $('#edit_mode').html("ON"); + $('#edit_mode').html('pixels'); $('#edit_brush_row').css('visibility', 'visible'); $('#edit_label_row').css('visibility', 'visible'); $('#edit_erase_row').css('visibility', 'visible'); - $('#edit_brush').html(brush_size); - $('#edit_label').html(edit_value); + $('#edit_brush').html(brush.size); + $('#edit_label').html(brush.value); - if (erase) { + if (brush.erase) { $('#edit_erase').html("ON"); } else { $('#edit_erase').html("OFF"); } } else { - $('#edit_mode').html("OFF"); + $('#edit_mode').html('whole labels'); $('#edit_brush_row').css('visibility', 'hidden'); $('#edit_label_row').css('visibility', 'hidden'); $('#edit_erase_row').css('visibility', 'hidden'); @@ -610,11 +622,9 @@ function render_info_display() { } function render_edit_image(ctx) { - let hidden_canvas = document.getElementById('hidden_canvas'); - - ctx.clearRect(0, 0, dimensions[0], dimensions[1]); - ctx.drawImage(raw_image, 0, 0, dimensions[0], dimensions[1]); - let image_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); + ctx.clearRect(padding, padding, dimensions[0], dimensions[1]); + ctx.drawImage(raw_image, padding, padding, dimensions[0], dimensions[1]); + let image_data = ctx.getImageData(padding, padding, dimensions[0], dimensions[1]); // adjust underlying raw image contrast_image(image_data, current_contrast); @@ -622,39 +632,43 @@ function render_edit_image(ctx) { if (display_invert) { invert(image_data); } - ctx.putImageData(image_data, 0, 0); + ctx.putImageData(image_data, padding, padding); + ctx.save(); ctx.globalCompositeOperation = 'color'; - ctx.drawImage(seg_image, 0, 0, dimensions[0], dimensions[1]); + ctx.drawImage(seg_image, padding, padding, dimensions[0], dimensions[1]); ctx.restore(); - // draw brushview on top of cells/annotations ctx.save(); - ctx.globalAlpha = 0.7; - ctx.globalCompositeOperation = 'source-over'; - ctx.drawImage(hidden_canvas, 0, 0, dimensions[0], dimensions[1]); + const region = new Path2D(); + region.rect(padding, padding, dimensions[0], dimensions[1]); + ctx.clip(region); + ctx.imageSmoothingEnabled = true; + + // draw brushview on top of cells/annotations + brush.draw(ctx, 0, 0, dimensions[0], dimensions[1], 1); ctx.restore(); } function render_raw_image(ctx) { - ctx.clearRect(0, 0, dimensions[0], dimensions[1]); - ctx.drawImage(raw_image, 0, 0, dimensions[0], dimensions[1]); + ctx.clearRect(padding, padding, dimensions[0], dimensions[1]); + ctx.drawImage(raw_image, padding, padding, dimensions[0], dimensions[1]); // contrast image - image_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); + image_data = ctx.getImageData(padding, padding, dimensions[0], dimensions[1]); contrast_image(image_data, current_contrast); // draw contrasted image over the original - ctx.putImageData(image_data, 0, 0); + ctx.putImageData(image_data, padding, padding); } function render_annotation_image(ctx) { - ctx.clearRect(0, 0, dimensions[0], dimensions[1]); - ctx.drawImage(seg_image, 0, 0, dimensions[0], dimensions[1]); + ctx.clearRect(padding, padding, dimensions[0], dimensions[1]); + ctx.drawImage(seg_image, padding, padding, dimensions[0], dimensions[1]); if (current_highlight) { - let img_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); + let img_data = ctx.getImageData(padding, padding, dimensions[0], dimensions[1]); highlight(img_data, mode.highlighted_cell_one); highlight(img_data, mode.highlighted_cell_two); - ctx.putImageData(img_data, 0, 0); + ctx.putImageData(img_data, padding, padding); } } @@ -694,8 +708,8 @@ function fetch_and_render_frame() { function load_file(file) { $.ajax({ - type:'POST', - url:"load/" + file, + type: 'POST', + url: 'load/' + file, success: function (payload) { max_frames = payload.max_frames; scale = payload.screen_scale; @@ -705,11 +719,8 @@ function load_file(file) { maxTrack = Math.max(... Object.keys(tracks).map(Number)); project_id = payload.project_id; - $('#canvas').get(0).width = dimensions[0]; - $('#canvas').get(0).height = dimensions[1]; - - $('#hidden_canvas').get(0).width = dimensions[0]; - $('#hidden_canvas').get(0).height = dimensions[1]; + $('#canvas').get(0).width = dimensions[0] + 2*padding; + $('#canvas').get(0).height = dimensions[1] + 2*padding; }, async: false }); @@ -718,9 +729,18 @@ function load_file(file) { // adjust current_contrast upon mouse scroll function handle_scroll(evt) { // adjust contrast whenever we can see raw - if (rendering_raw || edit_mode) { - let delta = - evt.originalEvent.deltaY / 2; - current_contrast = Math.max(current_contrast + delta, -100); + if ((rendering_raw || edit_mode) && !evt.originalEvent.shiftKey) { + // don't use magnitude of scroll + let mod_contrast = -Math.sign(evt.originalEvent.deltaY) * 4; + // stop if fully desaturated + current_contrast = Math.max(current_contrast + mod_contrast, -100); + // stop at 5x contrast + current_contrast = Math.min(current_contrast + mod_contrast, 400); + render_image_display(); + } else if ((rendering_raw || edit_mode) && evt.originalEvent.shiftKey) { + let mod = -Math.sign(evt.originalEvent.deltaY); + brightness = Math.min(brightness + mod, 255); + brightness = Math.max(brightness + mod, -512); render_image_display(); } } @@ -729,8 +749,8 @@ function handle_scroll(evt) { // of click&drag, since clicks are handled by Mode.click) function handle_mousedown(evt) { mousedown = true; - mouse_x = evt.offsetX; - mouse_y = evt.offsetY; + mouse_x = evt.offsetX - padding; + mouse_y = evt.offsetY - padding; // begin drawing if (edit_mode) { let img_y = Math.floor(mouse_y/scale); @@ -742,26 +762,24 @@ function handle_mousedown(evt) { // handles mouse movement, whether or not mouse button is held down function handle_mousemove(evt) { // update displayed info depending on where mouse is - mouse_x = evt.offsetX; - mouse_y = evt.offsetY; + mouse_x = evt.offsetX - padding; + mouse_y = evt.offsetY - padding; + brush.x = mouse_x; + brush.y = mouse_y; render_info_display(); // update brush preview if (edit_mode) { - // hidden canvas is keeping track of the brush - let hidden_canvas = document.getElementById('hidden_canvas'); - let hidden_ctx = $('#hidden_canvas').get(0).getContext("2d"); + // brush's canvas is keeping track of the brush if (mousedown) { // update mouse_trace let img_y = Math.floor(mouse_y/scale); let img_x = Math.floor(mouse_x/scale); mouse_trace.push([img_y, img_x]); } else { - hidden_ctx.clearRect(0,0,dimensions[0],dimensions[1]) + brush.clearView(); } - brush.x = mouse_x; - brush.y = mouse_y; - brush.draw(hidden_ctx); + brush.addToView(); render_image_display(); } } @@ -773,9 +791,7 @@ function handle_mouseup(evt) { //send click&drag coordinates to caliban.py to update annotations mode.handle_draw(); // reset brush preview - let hidden_canvas = document.getElementById('hidden_canvas'); - let hidden_ctx = $('#hidden_canvas').get(0).getContext("2d"); - hidden_ctx.clearRect(0, 0, dimensions[0], dimensions[1]); + brush.refreshView(); } } @@ -823,9 +839,17 @@ function action(action, info, frame = current_frame) { if (payload.imgs) { // load new value of seg_array // array of arrays, contains annotation data for frame - seg_array = payload.imgs.seg_arr; - seg_image.src = payload.imgs.segmented; - raw_image.src = payload.imgs.raw; + if (payload.imgs.hasOwnProperty('seg_arr')) { + seg_array = payload.imgs.seg_arr; + } + + if (payload.imgs.hasOwnProperty('segmented')) { + seg_image.src = payload.imgs.segmented; + } + + if (payload.imgs.hasOwnProperty('raw')) { + raw_image.src = payload.imgs.raw; + } } if (payload.tracks) { tracks = payload.tracks; @@ -840,21 +864,24 @@ function action(action, info, frame = current_frame) { } function start_caliban(filename) { + // disable scrolling from scrolling around on page (it should just control brightness) + document.addEventListener('wheel', function(event) { + event.preventDefault(); + }, {passive: false}); + // disable space and up/down keys from moving around on page + $(document).on('keydown', function(event) { + if (event.key === " ") { + event.preventDefault(); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + } else if (event.key === "ArrowDown") { + event.preventDefault(); + } + }); + load_file(filename); prepare_canvas(); fetch_and_render_frame(); - brush = { - x: 0, - y: 0, - radius: 1, - color: 'red', - draw: function(ctx) { - ctx.beginPath(); - ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true); - ctx.closePath(); - ctx.fillStyle = this.color; - ctx.fill(); - } - } -} \ No newline at end of file + brush = new Brush(scale=scale, height=dimensions[1], width=dimensions[0], pad = padding); +} diff --git a/browser/static/js/main_zstack.js b/browser/static/js/main_zstack.js index dcab51583..8fda703ca 100644 --- a/browser/static/js/main_zstack.js +++ b/browser/static/js/main_zstack.js @@ -5,37 +5,67 @@ class Mode { this.highlighted_cell_one = -1; this.highlighted_cell_two = -1; this.feature = 0; - this.channel = 0; + this._channel = 0; this.action = ""; this.prompt = ""; } + get channel() { + return this._channel; + } + + set channel(num) { + // don't try and change channel if no other channels exist + if (channelMax > 1) { + // save current display settings before changing + adjuster.brightnessMap.set(this._channel, adjuster.brightness); + adjuster.contrastMap.set(this._channel, adjuster.contrast); + adjuster.invertMap.set(this._channel, adjuster.displayInvert); + // change channel, wrap around if needed + if (num === channelMax) { + this._channel = 0; + } else if (num < 0) { + this._channel = channelMax - 1; + } else { + this._channel = num; + } + // get new channel image from server + this.info = {"channel": this._channel}; + action("change_channel", this.info); + this.clear(); + // get brightness/contrast vals for new channel + adjuster.brightness = adjuster.brightnessMap.get(this._channel); + adjuster.contrast = adjuster.contrastMap.get(this._channel); + adjuster.displayInvert = adjuster.invertMap.get(this._channel); + } + } + clear() { this.kind = Modes.none; this.info = {}; this.highlighted_cell_one = -1; this.highlighted_cell_two = -1; - thresholding = false; - target_value = 0; + brush.conv = false; + brush.clearThresh(); this.action = ""; this.prompt = ""; - render_image_display(); + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); } // these keybinds apply regardless of // edit_mode, mode.action, or mode.kind handle_universal_keybind(key) { - if (key === 'a' || key === 'ArrowLeft') { + if (!rgb && (key === 'a' || key === 'ArrowLeft')) { // go backward one frame current_frame -= 1; if (current_frame < 0) { current_frame = max_frames - 1; } fetch_and_render_frame(); - } else if (key === 'd' || key === 'ArrowRight') { + } else if (!rgb && (key === 'd' || key === 'ArrowRight')) { // go forward one frame current_frame += 1; if (current_frame >= max_frames) { @@ -45,14 +75,25 @@ class Mode { } else if (key === "Escape") { // deselect/cancel action/reset highlight mode.clear(); - } else if (key === 'h') { + // may want some things here that trigger on ESC but not clear() + } else if (!rgb && key === 'h') { // toggle highlight current_highlight = !current_highlight; - render_image_display(); + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); } else if (key === 'z') { // toggle rendering_raw rendering_raw = !rendering_raw; render_image_display(); + } else if (key === '0') { + // reset brightness adjustments + adjuster.resetBrightnessContrast(); + } else if ((key === 'l' || key === 'L') && rgb && !edit_mode) { + display_labels = !display_labels; + render_image_display(); + } else if (key === '-') { + changeZoom(1); + } else if (key === '=') { + changeZoom(-1); } } @@ -61,67 +102,44 @@ class Mode { handle_universal_edit_keybind(key) { if (key === "ArrowDown") { // decrease brush size, minimum size 1 - brush_size = Math.max(brush_size - 1, 1); - - // update the brush with its new size - clear_hidden_ctx(); - brush.radius = brush_size * scale; - brush.draw(hidden_ctx); - + brush.size -= 1; // redraw the frame with the updated brush preview render_image_display(); } else if (key === "ArrowUp") { - //increase brush size, shouldn't be larger than the image - brush_size = Math.min(self.brush_size + 1, - dimensions[0]/scale, dimensions[1]/scale); - - // update the brush with its new size - clear_hidden_ctx(); - brush.radius = brush_size * scale; - brush.draw(hidden_ctx); - + // increase brush size, diameter shouldn't be larger than the image + brush.size += 1; // redraw the frame with the updated brush preview render_image_display(); - } else if (key === 'i') { + } else if (!rgb && key === 'i') { // toggle light/dark inversion of raw img - display_invert = !display_invert; + adjuster.toggleInvert(); + } else if (!rgb && settings.pixel_only && (key === 'l' || key === 'L')) { + display_labels = !display_labels; render_image_display(); } else if (key === 'n') { // set edit value to something unused - edit_value = maxLabelsMap.get(this.feature) + 1; - update_seg_highlight(); - if (this.kind === Modes.prompt) { - erase = false; - this.prompt = "Now drawing over label " + target_value + " with label " + edit_value + brush.value = maxLabelsMap.get(this.feature) + 1; + if (this.kind === Modes.prompt && brush.conv) { + this.prompt = "Now drawing over label " + brush.target + " with label " + brush.value + ". Use ESC to leave this mode."; this.kind = Modes.drawing; } - render_info_display(); + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); } } // keybinds that apply when in edit mode handle_edit_keybind(key) { - if (key === "e") { + if (key === "e" && !settings.pixel_only) { // toggle edit mode edit_mode = !edit_mode; - render_image_display(); + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); } else if (key === "c") { // cycle forward one channel, if applicable - if (channel_max > 1) { - this.channel = this.increment_value(this.channel, 0, channel_max -1); - this.info = {"channel": this.channel}; - action("change_channel", this.info); - this.clear(); - } + this.channel += 1; } else if (key === "C") { // cycle backward one channel, if applicable - if (channel_max > 1) { - this.channel = this.decrement_value(this.channel, 0, channel_max -1); - this.info = {"channel": this.channel}; - action("change_channel", this.info); - this.clear(); - } + this.channel -= 1; } else if (key === "f") { // cycle forward one feature, if applicable if (feature_max > 1) { @@ -138,21 +156,25 @@ class Mode { action("change_feature", this.info); this.clear(); } - } else if (key === "=") { + } else if (key === "]") { // increase edit_value up to max label + 1 (guaranteed unused) - edit_value = Math.min(edit_value + 1, - maxLabelsMap.get(this.feature) + 1); - update_seg_highlight(); + brush.value = Math.min(brush.value + 1, + maxLabelsMap.get(this.feature) + 1); + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } render_info_display(); - } else if (key === "-") { + } else if (key === "[") { // decrease edit_value, minimum 1 - edit_value = Math.max(edit_value - 1, 1); - update_seg_highlight(); + brush.value -= 1; + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } render_info_display(); } else if (key === "x") { // turn eraser on and off - erase = !erase; - render_info_display(); + brush.erase = !brush.erase; + render_image_display(); } else if (key === 'p') { // color picker this.kind = Modes.prompt; @@ -164,41 +186,32 @@ class Mode { this.kind = Modes.prompt; this.action = "pick_target"; this.prompt = "First, click on the label you want to overwrite."; - render_info_display(); - } else if (key === 't') { + brush.conv = true; + render_image_display(); + } else if (key === 't' && !rgb) { // prompt thresholding with bounding box this.kind = Modes.question; this.action = "start_threshold"; this.prompt = "Click and drag to create a bounding box around the area you want to threshold"; - thresholding = true; - clear_hidden_ctx(); + brush.show = false; + brush.clearView(); render_image_display(); } } // keybinds that apply in bulk mode, nothing selected handle_mode_none_keybind(key) { - if (key === "e") { + if (key === "e" && !settings.label_only) { // toggle edit mode edit_mode = !edit_mode; helper_brush_draw(); - render_image_display(); + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); } else if (key === "c") { // cycle forward one channel, if applicable - if (channel_max > 1) { - this.channel = this.increment_value(this.channel, 0, channel_max -1); - this.info = {"channel": this.channel}; - action("change_channel", this.info); - this.clear(); - } + this.channel += 1; } else if (key === "C") { // cycle backward one channel, if applicable - if (channel_max > 1) { - this.channel = this.decrement_value(this.channel, 0, channel_max -1); - this.info = {"channel": this.channel}; - action("change_channel", this.info); - this.clear(); - } + this.channel -= 1; } else if (key === "f") { // cycle forward one feature, if applicable if (feature_max > 1) { @@ -215,36 +228,42 @@ class Mode { action("change_feature", this.info); this.clear(); } - } else if (key === "p") { + } else if (key === "p" && !rgb) { //iou cell identity prediction this.kind = Modes.question; this.action = "predict"; this.prompt = "Predict cell ids for zstack? / S=PREDICT THIS FRAME / SPACE=PREDICT ALL FRAMES / ESC=CANCEL PREDICTION"; render_info_display(); - } else if (key === "-" && this.highlighted_cell_one !== -1) { + } else if (key === "[" && this.highlighted_cell_one !== -1) { // cycle highlight to prev label this.highlighted_cell_one = this.decrement_value(this.highlighted_cell_one, 1, maxLabelsMap.get(this.feature)); - render_image_display(); - } else if (key === "=" && this.highlighted_cell_one !== -1) { + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } + } else if (key === "]" && this.highlighted_cell_one !== -1) { // cycle highlight to next label this.highlighted_cell_one = this.increment_value(this.highlighted_cell_one, 1, maxLabelsMap.get(this.feature)); - render_image_display(); + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } } } // keybinds that apply in bulk mode, one selected handle_mode_single_keybind(key) { - if (key === "f") { + if (key === "f" && !rgb) { //hole fill - this.info = { "label": this.info.label, - "frame": current_frame}; + this.info = { + "label": this.info.label, + "frame": current_frame + }; this.kind = Modes.prompt; this.action = "fill_hole"; this.prompt = "Select hole to fill in cell " + this.info.label; render_info_display(); - } else if (key === "c") { + } else if (!rgb && key === "c") { // create new this.kind = Modes.question; this.action = "create_new"; @@ -253,10 +272,10 @@ class Mode { } else if (key === "x") { // delete label from frame this.kind = Modes.question; - this.action = "delete"; + this.action = "delete_mask"; this.prompt = "delete label " + this.info.label + " in frame " + this.info.frame + "? " + answer; render_info_display(); - } else if (key === "-") { + } else if (key === "[") { // cycle highlight to prev label this.highlighted_cell_one = this.decrement_value(this.highlighted_cell_one, 1, maxLabelsMap.get(this.feature)); @@ -264,8 +283,10 @@ class Mode { let temp_highlight = this.highlighted_cell_one; this.clear(); this.highlighted_cell_one = temp_highlight; - render_image_display(); - } else if (key === "=") { + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } + } else if (key === "]") { // cycle highlight to next label this.highlighted_cell_one = this.increment_value(this.highlighted_cell_one, 1, maxLabelsMap.get(this.feature)); @@ -273,7 +294,9 @@ class Mode { let temp_highlight = this.highlighted_cell_one; this.clear(); this.highlighted_cell_one = temp_highlight; - render_image_display(); + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } } } @@ -286,13 +309,13 @@ class Mode { this.prompt = ("Replace " + this.info.label_2 + " with " + this.info.label_1 + "? // SPACE = Replace in all frames / S = Replace in this frame only / ESC = Cancel replace"); render_info_display(); - } else if (key === "s") { + } else if (!rgb && key === "s") { // swap this.kind = Modes.question; this.action = "swap_cells"; this.prompt = "SPACE = SWAP IN ALL FRAMES / S = SWAP IN THIS FRAME ONLY / ESC = CANCEL SWAP"; render_info_display(); - } else if (key === "w" ) { + } else if (key === "w" && !rgb) { // watershed this.kind = Modes.question; this.action = "watershed"; @@ -304,14 +327,14 @@ class Mode { // keybinds that apply in bulk mode, answering question/prompt handle_mode_question_keybind(key) { if (key === " ") { - if (this.action === "flood_cell") { - action("flood_cell", this.info); + if (this.action === "flood_contiguous") { + action("flood_contiguous", this.info); } else if (this.action === "trim_pixels") { action("trim_pixels", this.info); } else if (this.action === "create_new") { action("new_cell_stack", this.info); - } else if (this.action === "delete") { - action("delete", this.info); + } else if (this.action === "delete_mask") { + action("delete_mask", this.info); } else if (this.action === "predict") { action("predict_zstack", this.info); } else if (this.action === "replace") { @@ -388,37 +411,37 @@ class Mode { } handle_draw() { - action("handle_draw", { "trace": JSON.stringify(mouse_trace), //stringify array so it doesn't get messed up - "target_value": target_value, //value that we're overwriting - "brush_value": edit_value, //we don't update caliban with edit_value, etc each time they change - "brush_size": brush_size, //so we need to pass them in as args - "erase": erase, - "frame": current_frame}); + action("handle_draw", { + "trace": JSON.stringify(mouse_trace), // stringify array so it doesn't get messed up + "target_value": brush.target, // value that we're overwriting + "brush_value": brush.value, // we don't update caliban with edit_value, etc each time they change + "brush_size": brush.size, // so we need to pass them in as args + "erase": (brush.erase && !brush.conv), + "frame": current_frame + }); mouse_trace = []; if (this.kind !== Modes.drawing) { this.clear(); } } - handle_threshold(evt) { - thresholding = false; - let end_y = evt.offsetY; - let end_x = evt.offsetX; - - let threshold_start_y = Math.floor(box_start_y / scale); - let threshold_start_x = Math.floor(box_start_x / scale); - let threshold_end_y = Math.floor(end_y / scale); - let threshold_end_x = Math.floor(end_x / scale); + handle_threshold() { + let threshold_start_y = brush.threshY; + let threshold_start_x = brush.threshX; + let threshold_end_x = imgX; + let threshold_end_y = imgY; if (threshold_start_y !== threshold_end_y && threshold_start_x !== threshold_end_x) { - action("threshold", {"y1": threshold_start_y, - "x1": threshold_start_x, - "y2": threshold_end_y, - "x2": threshold_end_x, - "frame": current_frame, - "label": maxLabelsMap.get(this.feature) + 1}); + action("threshold", { + "y1": threshold_start_y, + "x1": threshold_start_x, + "y2": threshold_end_y, + "x2": threshold_end_x, + "frame": current_frame, + "label": maxLabelsMap.get(this.feature) + 1 + }); } this.clear(); render_image_display(); @@ -448,59 +471,65 @@ class Mode { if (evt.altKey) { // alt+click this.kind = Modes.question; - this.action = "flood_cell"; - this.info = {"label": current_label, - "frame": current_frame, - "x_location": mouse_x, - "y_location": mouse_y}; + this.action = "flood_contiguous"; + this.info = { + "label": current_label, + "frame": current_frame, + "x_location": imgX, + "y_location": imgY + }; this.prompt = "SPACE = FLOOD SELECTED CELL WITH NEW LABEL / ESC = CANCEL"; this.highlighted_cell_one = current_label; } else if (evt.shiftKey) { // shift+click this.kind = Modes.question; this.action = "trim_pixels"; - this.info = {"label": current_label, - "frame": current_frame, - "x_location": mouse_x, - "y_location": mouse_y}; + this.info = { + "label": current_label, + "frame": current_frame, + "x_location": imgX, + "y_location": imgY + }; this.prompt = "SPACE = TRIM DISCONTIGUOUS PIXELS FROM CELL / ESC = CANCEL"; this.highlighted_cell_one = current_label; } else { // normal click this.kind = Modes.single; - this.info = { "label": current_label, - "frame": current_frame }; + this.info = { + "label": current_label, + "frame": current_frame + }; this.highlighted_cell_one = current_label; this.highlighted_cell_two = -1; - temp_x = mouse_x; - temp_y = mouse_y; + storedClickX = imgX; + storedClickY = imgY; } } handle_mode_prompt_click(evt) { if (this.action === "fill_hole" && current_label === 0) { - this.info = { "label": this.info.label, - "frame": current_frame, - "x_location": mouse_x, - "y_location": mouse_y }; + this.info = { + "label": this.info.label, + "frame": current_frame, + "x_location": imgX, + "y_location": imgY + }; action(this.action, this.info); this.clear(); } else if (this.action === "pick_color" && current_label !== 0 - && current_label !== target_value) { - edit_value = current_label; - update_seg_highlight(); - if (target_value !== 0) { - erase = false; - this.prompt = "Now drawing over label " + target_value + " with label " + edit_value + && current_label !== brush.target) { + brush.value = current_label; + if (brush.target !== 0) { + this.prompt = "Now drawing over label " + brush.target + " with label " + brush.value + ". Use ESC to leave this mode."; this.kind = Modes.drawing; - render_info_display(); + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); } else { this.clear(); } } else if (this.action === "pick_target" && current_label !== 0) { - target_value = current_label; + brush.target = current_label; this.action = "pick_color"; this.prompt = "Click on the label you want to draw with, or press 'n' to draw with an unused label."; render_info_display(); @@ -513,28 +542,31 @@ class Mode { this.highlighted_cell_one = this.info.label; this.highlighted_cell_two = current_label; - this.info = { "label_1": this.info.label, - "label_2": current_label, - "frame_1": this.info.frame, - "frame_2": current_frame, - "x1_location": temp_x, - "y1_location": temp_y, - "x2_location": mouse_x, - "y2_location": mouse_y }; + this.info = { + "label_1": this.info.label, + "label_2": current_label, + "frame_1": this.info.frame, + "frame_2": current_frame, + "x1_location": storedClickX, + "y1_location": storedClickY, + "x2_location": imgX, + "y2_location": imgY + }; } handle_mode_multiple_click(evt) { this.highlighted_cell_one = this.info.label_1; this.highlighted_cell_two = current_label; - - this.info = {"label_1": this.info.label_1, - "label_2": current_label, - "frame_1": this.info.frame_1, - "frame_2": current_frame, - "x1_location": temp_x, - "y1_location": temp_y, - "x2_location": mouse_x, - "y2_location": mouse_y}; + this.info = { + "label_1": this.info.label_1, + "label_2": current_label, + "frame_1": this.info.frame_1, + "frame_2": current_frame, + "x1_location": storedClickX, + "y1_location": storedClickY, + "x2_location": imgX, + "y2_location": imgY + }; } click(evt) { @@ -548,15 +580,27 @@ class Mode { } else if (this.kind === Modes.none) { //if nothing selected: shift-, alt-, or normal click this.handle_mode_none_click(evt); - render_image_display(); + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } else { + render_info_display(); + } } else if (this.kind === Modes.single) { // one label already selected this.handle_mode_single_click(evt); - render_image_display(); + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } else { + render_info_display(); + } } else if (this.kind === Modes.multiple) { // two labels already selected, reselect second label this.handle_mode_multiple_click(evt); - render_image_display(); + if (current_highlight) { + adjuster.preCompAdjust(seg_array, current_highlight, edit_mode, brush, this); + } else { + render_info_display(); + } } } @@ -577,10 +621,6 @@ class Mode { } } - - - - var Modes = Object.freeze({ "none": 1, "single": 2, @@ -591,126 +631,155 @@ var Modes = Object.freeze({ "drawing": 7 }); -var temp_x = 0; -var temp_y = 0; +let rgb; + +// dimensions of raw arrays +let rawWidth; +let rawHeight; + +var scale; +const padding = 5; + +// mouse position variables +// mouse position on canvas, no adjustment for padding +let _rawMouseX; +let _rawMouseY; +// adjusted for padding +let canvasPosX; +let canvasPosY; +// coordinates in original image (used for actions, labels, etc) +let imgX; +let imgY; +// in original image coords +let storedClickX; +let storedClickY; + +// zoom, starts at 100 percent (value set in ___) +let zoom; +// farthest amount to zoom out +let zoomLimit; + +// starting indices (original coords) for displaying image +// (starts at 0, values set in ______) +let sx; +let sy; +// how far past starting indices to display +let swidth; +let sheight; + +var seg_array; // declare here so it is global var + +let topBorder = new Path2D(); +let bottomBorder = new Path2D(); +let rightBorder = new Path2D(); +let leftBorder = new Path2D(); + var rendering_raw = false; -let display_invert = true; -var current_contrast = 0; +let display_labels; + var current_frame = 0; var current_label = 0; -var current_highlight = false; +var current_highlight; var max_frames; var feature_max; -var channel_max; +var channelMax; var dimensions; var tracks; let maxLabelsMap = new Map(); var mode = new Mode(Modes.none, {}); -var raw_image = new Image(); -raw_image.onload = render_image_display; -var seg_image = new Image(); -seg_image.onload = update_seg_highlight; -var seg_array; // declare here so it is global var -var scale; -var mouse_x = 0; -var mouse_y = 0; -var edit_mode = false; -let edit_value = 1; -let target_value = 0; -var brush_size = 1; -var erase = false; +let edit_mode; var answer = "(SPACE=YES / ESC=NO)"; let mousedown = false; +let spacedown = false; var tooltype = 'draw'; var project_id; -var brush; let mouse_trace = []; -let thresholding = false; -let box_start_x; -let box_start_y; -let hidden_ctx; -const adjusted_seg = new Image(); -adjusted_seg.onload = render_image_display; - -function upload_file() { + +var brush; +var adjust; + +var waitForFinalEvent = (function () { + var timers = {}; + return function (callback, ms, uniqueId) { + if (!uniqueId) { + uniqueId = "Don't call this twice without a uniqueId"; + } + if (timers[uniqueId]) { + clearTimeout (timers[uniqueId]); + } + timers[uniqueId] = setTimeout(callback, ms); + }; +})(); + +function upload_file(cb) { $.ajax({ - type:'POST', - url:"upload_file/" + project_id, - success: function (payload) { - }, - async: false + type: 'POST', + url: `upload_file/${project_id}`, + success: cb, + async: true }); } -// image adjustment functions: take img as input and manipulate data attribute -// pixel data is 1D array of 8bit RGBA values -function contrast_image(img, contrast) { - let d = img.data; - contrast = (contrast / 100) + 1; - /* let intercept = 128 * (1 - contrast); */ - for (let i = 0; i < d.length; i += 4) { - d[i] *= contrast; - d[i + 1] *= contrast; - d[i + 2] *= contrast; +// based on dx and dy, update sx and sy +function panCanvas(dx, dy) { + let tempPanX = sx - dx; + let tempPanY = sy - dy; + let oldY = sy; + let oldX = sx; + if (tempPanX >= 0 && tempPanX + swidth < rawWidth) { + sx = tempPanX; + } else { + tempPanX = Math.max(0, tempPanX); + sx = Math.min(rawWidth - swidth, tempPanX); } - return img; -} - -function highlight(img, label) { - let ann = img.data; - - // use label array to figure out which pixels to recolor - for (var j = 0; j < seg_array.length; j += 1){ //y - for (var i = 0; i < seg_array[j].length; i += 1){ //x - let jlen = seg_array[j].length; - - if (seg_array[j][i] === label){ - // fill in all pixels affected by scale - // k and l get the pixels that are part of the original pixel that has been scaled up - for (var k = 0; k < scale; k +=1) { - for (var l = 0; l < scale; l +=1) { - // location in 1D array based on i,j, and scale - pixel_num = (scale*(jlen*(scale*j + l) + i)) + k; - - // set to red by changing RGB values - ann[(pixel_num*4)] = 255; - ann[(pixel_num*4) + 1] = 0; - ann[(pixel_num*4) + 2] = 0; - } - } - } - } + if (tempPanY >= 0 && tempPanY + sheight < rawHeight) { + sy = tempPanY; + } else { + tempPanY = Math.max(0, tempPanY); + sy = Math.min(rawHeight - sheight, tempPanY); + } + if (sx !== oldX || sy !== oldY) { + render_image_display(); } } -function grayscale(img) { - let data = img.data; - for (var i = 0; i < data.length; i += 4) { - var avg = (data[i] + data[i + 1] + data[i + 2]) / 3; - data[i] = avg; // red - data[i + 1] = avg; // green - data[i + 2] = avg; // blue - } - return img; +function changeZoom(dzoom) { + let newZoom = zoom - 10*dzoom; + let oldZoom = zoom; + let newHeight = rawHeight*100/newZoom; + let newWidth = rawWidth*100/newZoom; + let oldHeight = sheight; + let oldWidth = swidth; + if (newZoom >= zoomLimit) { + zoom = newZoom; + sheight = newHeight; + swidth = newWidth; + } + if (oldZoom !== newZoom) { + let propX = canvasPosX/dimensions[0]; + let propY = canvasPosY/dimensions[1]; + let dx = propX*(newWidth - oldWidth); + let dy = propY*(newHeight - oldHeight); + panCanvas(dx, dy); + } + updateMousePos(_rawMouseX, _rawMouseY); + render_image_display(); } -function invert(img) { - let data = img.data; - for (var i = 0; i < data.length; i += 4) { - data[i] = 255 - data[i]; // red - data[i + 1] = 255 - data[i + 1]; // green - data[i + 2] = 255 - data[i + 2]; // blue - } - return img; +// check if the mouse position in canvas matches to a displayed part of image +function inRange(x, y) { + if (x >= 0 && x < dimensions[0] && + y >= 0 && y < dimensions[1]) { + return true; + } else { + return false; + } } function label_under_mouse() { - let img_y = Math.floor(mouse_y/scale); - let img_x = Math.floor(mouse_x/scale); let new_label; - if (img_y >= 0 && img_y < seg_array.length && - img_x >= 0 && img_x < seg_array[0].length) { - new_label = seg_array[img_y][img_x]; //check array value at mouse location + if (inRange(canvasPosX, canvasPosY)) { + new_label = Math.abs(seg_array[imgY][imgX]); //check array value at mouse location } else { new_label = 0; } @@ -721,7 +790,11 @@ function render_highlight_info() { if (current_highlight) { $('#highlight').html("ON"); if (edit_mode) { - $('#currently_highlighted').html(edit_value) + if (brush.value > 0) { + $('#currently_highlighted').html(brush.value) + } else { + $('#currently_highlighted').html('-') + } } else { if (mode.highlighted_cell_one !== -1) { if (mode.highlighted_cell_two !== -1) { @@ -741,22 +814,26 @@ function render_highlight_info() { function render_edit_info() { if (edit_mode) { - $('#edit_mode').html("ON"); + $('#edit_mode').html('pixels'); $('#edit_brush_row').css('visibility', 'visible'); $('#edit_label_row').css('visibility', 'visible'); $('#edit_erase_row').css('visibility', 'visible'); - $('#edit_brush').html(brush_size); - $('#edit_label').html(edit_value); + $('#edit_brush').html(brush.size); + if (brush.value > 0) { + $('#edit_label').html(brush.value); + } else { + $('#edit_label').html('-'); + } - if (erase) { + if (brush.erase && !brush.conv) { $('#edit_erase').html("ON"); } else { $('#edit_erase').html("OFF"); } } else { - $('#edit_mode').html("OFF"); + $('#edit_mode').html('whole labels'); $('#edit_brush_row').css('visibility', 'hidden'); $('#edit_label_row').css('visibility', 'hidden'); $('#edit_erase_row').css('visibility', 'hidden'); @@ -781,6 +858,9 @@ function render_info_display() { $('#frame').html(current_frame); $('#feature').html(mode.feature); $('#channel').html(mode.channel); + $('#zoom').html(`${zoom}%`); + $('#displayedX').html(`${Math.floor(sx)}-${Math.ceil(sx+swidth)}`); + $('#displayedY').html(`${Math.floor(sy)}-${Math.ceil(sy+sheight)}`); render_highlight_info(); @@ -792,86 +872,84 @@ function render_info_display() { $('#mode').html(mode.render()); } -function clear_hidden_ctx() { - hidden_ctx.clearRect(0,0,dimensions[0],dimensions[1]); -} - -// apply highlight to edit_value in seg_image, save resulting -// image as src of adjusted_seg to use to render edit (if needed) -// additional hidden canvas is used to prevent image flickering -function update_seg_highlight() { - let canvas = document.getElementById('hidden_seg_canvas'); - let ctx = $('#hidden_seg_canvas').get(0).getContext("2d"); - ctx.imageSmoothingEnabled = false; - - // draw seg_image so we can extract image data - ctx.clearRect(0, 0, dimensions[0], dimensions[1]); - ctx.drawImage(seg_image, 0, 0, dimensions[0], dimensions[1]); - let seg_img_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); - highlight(seg_img_data, edit_value); - ctx.putImageData(seg_img_data, 0, 0); - // once this new src is loaded, displayed image will be rerendered - adjusted_seg.src = canvas.toDataURL(); -} - function render_edit_image(ctx) { - ctx.clearRect(0, 0, dimensions[0], dimensions[1]); - ctx.drawImage(raw_image, 0, 0, dimensions[0], dimensions[1]); - let raw_image_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); - - // adjust underlying raw image - contrast_image(raw_image_data, current_contrast); - grayscale(raw_image_data); - if (display_invert) { - invert(raw_image_data); - } - ctx.putImageData(raw_image_data, 0, 0); - - // draw segmentations, highlighted version if highlight is on - ctx.save(); - // ctx.globalCompositeOperation = 'color'; - ctx.globalAlpha = 0.3; - if (current_highlight) { - ctx.drawImage(adjusted_seg, 0, 0, dimensions[0], dimensions[1]); + if (rgb && rendering_raw) { + render_raw_image(ctx); + } else if (!rgb && !display_labels) { + ctx.clearRect(padding, padding, dimensions[0], dimensions[1]); + ctx.drawImage(adjuster.preCompRaw, sx, sy, swidth, sheight, padding, padding, dimensions[0], dimensions[1]); } else { - ctx.drawImage(seg_image, 0, 0, dimensions[0], dimensions[1]); + ctx.clearRect(padding, padding, dimensions[0], dimensions[1]); + ctx.drawImage(adjuster.postCompImg, sx, sy, swidth, sheight, padding, padding, dimensions[0], dimensions[1]); } - ctx.restore(); + ctx.save(); + let region = new Path2D(); + region.rect(padding, padding, dimensions[0], dimensions[1]); + ctx.clip(region); + ctx.imageSmoothingEnabled = true; // draw brushview on top of cells/annotations - ctx.save(); - ctx.globalAlpha = 0.2; - ctx.globalCompositeOperation = 'source-over'; - let hidden_canvas = document.getElementById('hidden_canvas'); - ctx.drawImage(hidden_canvas, 0,0,dimensions[0],dimensions[1]); + brush.draw(ctx, sx, sy, swidth, sheight, scale*zoom/100); + ctx.restore(); } function render_raw_image(ctx) { - ctx.clearRect(0, 0, dimensions, dimensions[1]); - ctx.drawImage(raw_image, 0, 0, dimensions[0], dimensions[1]); - - // contrast image - image_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); - contrast_image(image_data, current_contrast); - // draw contrasted image over the original - ctx.putImageData(image_data, 0, 0); + ctx.clearRect(padding, padding, dimensions, dimensions[1]); + ctx.drawImage(adjuster.contrastedRaw, sx, sy, swidth, sheight, padding, padding, dimensions[0], dimensions[1]); } function render_annotation_image(ctx) { - ctx.clearRect(0, 0, dimensions[0], dimensions[1]); - ctx.drawImage(seg_image, 0, 0, dimensions[0], dimensions[1]); - if (current_highlight) { - let img_data = ctx.getImageData(0, 0, dimensions[0], dimensions[1]); - highlight(img_data, mode.highlighted_cell_one); - highlight(img_data, mode.highlighted_cell_two); - ctx.putImageData(img_data, 0, 0); + ctx.clearRect(padding, padding, dimensions[0], dimensions[1]); + if (rgb && !display_labels) { + ctx.drawImage(adjuster.postCompImg, sx, sy, swidth, sheight, padding, padding, dimensions[0], dimensions[1]); + } else { + ctx.drawImage(adjuster.preCompSeg, sx, sy, swidth, sheight, padding, padding, dimensions[0], dimensions[1]); + } +} + +function drawBorders(ctx) { + ctx.save(); + // left border + if (Math.floor(sx) === 0) { + ctx.fillStyle = 'white'; + } else { + ctx.fillStyle = 'black'; + } + ctx.fill(leftBorder); + + // right border + if (Math.ceil(sx + swidth) === rawWidth) { + ctx.fillStyle = 'white'; + } else { + ctx.fillStyle = 'black'; } + ctx.fill(rightBorder); + + // top border + if (Math.floor(sy) === 0) { + ctx.fillStyle = 'white'; + } else { + ctx.fillStyle = 'black'; + } + ctx.fill(topBorder); + + // bottom border + if (Math.ceil(sy + sheight) === rawHeight) { + ctx.fillStyle = 'white'; + } else { + ctx.fillStyle = 'black'; + } + ctx.fill(bottomBorder); + + ctx.restore(); } function render_image_display() { - let ctx = $('#canvas').get(0).getContext("2d"); + let ctx = $('#canvas').get(0).getContext('2d'); ctx.imageSmoothingEnabled = false; + ctx.save(); + ctx.clearRect(0, 0, 2 * padding + dimensions[0], 2 * padding + dimensions[1]); if (edit_mode) { // edit mode (annotations overlaid on raw + brush preview) @@ -883,6 +961,7 @@ function render_image_display() { // draw annotations render_annotation_image(ctx); } + drawBorders(ctx); render_info_display(); } @@ -891,11 +970,14 @@ function fetch_and_render_frame() { type: 'GET', url: "frame/" + current_frame + "/" + project_id, success: function(payload) { + adjuster.rawLoaded = false; + adjuster.segLoaded = false; + // load new value of seg_array // array of arrays, contains annotation data for frame seg_array = payload.seg_arr; - seg_image.src = payload.segmented; - raw_image.src = payload.raw; + adjuster.segImage.src = payload.segmented; + adjuster.rawImage.src = payload.raw; }, async: false }); @@ -903,127 +985,225 @@ function fetch_and_render_frame() { function load_file(file) { $.ajax({ - type:'POST', - url:"load/" + file, + type: 'POST', + url: `load/${file}?&rgb=${settings.rgb}`, success: function (payload) { max_frames = payload.max_frames; feature_max = payload.feature_max; - channel_max = payload.channel_max; - scale = payload.screen_scale; - dimensions = [scale * payload.dimensions[0], scale * payload.dimensions[1]]; + channelMax = payload.channel_max; + rawDimensions = payload.dimensions; + + sx = 0; + sy = 0; + swidth = rawWidth = rawDimensions[0]; + sheight = rawHeight = rawDimensions[1]; - tracks = payload.tracks; //tracks payload is dict + setCanvasDimensions(rawDimensions); - //for each feature, get list of cell labels that are in that feature - //(each is a key in that dict), cast to numbers, then get the maximum - //value from each array and store it in a map + tracks = payload.tracks; // tracks payload is dict + + // for each feature, get list of cell labels that are in that feature + // (each is a key in that dict), cast to numbers, then get the maximum + // value from each array and store it in a map for (let i = 0; i < Object.keys(tracks).length; i++){ - let key = Object.keys(tracks)[i]; //the keys are strings - //use i as key in this map because it is an int, mode.feature is also int - maxLabelsMap.set(i, Math.max(... Object.keys(tracks[key]).map(Number))); + let key = Object.keys(tracks)[i]; // the keys are strings + if (Object.keys(tracks[key]).length > 0) { + // use i as key in this map because it is an int, mode.feature is also int + maxLabelsMap.set(i, Math.max(... Object.keys(tracks[key]).map(Number))); + } else { + // if no labels in feature, explicitly set max label to 0 + maxLabelsMap.set(i, 0); + } } - project_id = payload.project_id; - $('#canvas').get(0).width = dimensions[0]; - $('#canvas').get(0).height = dimensions[1]; - $('#hidden_canvas').get(0).width = dimensions[0]; - $('#hidden_canvas').get(0).height = dimensions[1]; - $('#hidden_seg_canvas').get(0).width = dimensions[0]; - $('#hidden_seg_canvas').get(0).height = dimensions[1]; }, async: false }); } -// adjust current_contrast upon mouse scroll +function setCanvasDimensions(rawDims) { + // calculate available space and how much to scale x and y to fill it + // only thing that shares width is the info display on left + + let maxWidth = Math.floor( + document.getElementsByTagName('main')[0].clientWidth - + parseInt($('main').css('marginTop')) - + parseInt($('main').css('marginBottom')) - + document.getElementById('table-col').clientWidth - + parseFloat($('#table-col').css('padding-left')) - + parseFloat($('#table-col').css('padding-right')) - + parseFloat($('#table-col').css('margin-left')) - + parseFloat($('#table-col').css('margin-right')) - + parseFloat($('#canvas-col').css('padding-left')) - + parseFloat($('#canvas-col').css('padding-right')) - + parseFloat($('#canvas-col').css('margin-left')) - + parseFloat($('#canvas-col').css('margin-right')) + ); + + // leave space for navbar, instructions pane, and footer + let maxHeight = Math.floor( + ( + ( + window.innerHeight || + document.documentElement.clientHeight || + document.body.clientHeight + ) - + parseInt($('main').css('marginTop')) - + parseInt($('main').css('marginBottom')) - + document.getElementsByClassName('page-footer')[0].clientHeight - + document.getElementsByClassName('collapsible')[0].clientHeight - + document.getElementsByClassName('navbar-fixed')[0].clientHeight + ) + ); + + let scaleX = maxWidth / rawDims[0]; + let scaleY = maxHeight / rawDims[1]; + + // pick scale that accomodates both dimensions; can be less than 1 + scale = Math.min(scaleX, scaleY); + // dimensions need to maintain aspect ratio for drawing purposes + dimensions = [scale * rawDims[0], scale * rawDims[1]]; + + zoom = 100; + zoomLimit = 100; + + // set canvases size according to scale + $('#canvas').get(0).width = dimensions[0] + 2 * padding; + $('#canvas').get(0).height = dimensions[1] + 2 * padding; + + // create paths for recoloring borders + topBorder = new Path2D(); + topBorder.moveTo(0, 0); + topBorder.lineTo(padding, padding); + topBorder.lineTo(dimensions[0] + padding, padding); + topBorder.lineTo(dimensions[0] + 2 * padding, 0); + topBorder.closePath(); + + bottomBorder = new Path2D(); + bottomBorder.moveTo(0, dimensions[1] + 2 * padding); + bottomBorder.lineTo(padding, dimensions[1] + padding); + bottomBorder.lineTo(dimensions[0] + padding, dimensions[1] + padding); + bottomBorder.lineTo(dimensions[0] + 2 * padding, dimensions[1] + 2 * padding); + bottomBorder.closePath(); + + leftBorder = new Path2D(); + leftBorder.moveTo(0, 0); + leftBorder.lineTo(0, dimensions[1] + 2 * padding); + leftBorder.lineTo(padding, dimensions[1] + padding); + leftBorder.lineTo(padding, padding); + leftBorder.closePath(); + + rightBorder = new Path2D(); + rightBorder.moveTo(dimensions[0] + 2 * padding, 0); + rightBorder.lineTo(dimensions[0] + padding, padding); + rightBorder.lineTo(dimensions[0] + padding, dimensions[1] + padding); + rightBorder.lineTo(dimensions[0] + 2 * padding, dimensions[1] + 2 * padding); + rightBorder.closePath(); +} + +// adjust contrast, brightness, or zoom upon mouse scroll function handle_scroll(evt) { - // adjust contrast whenever we can see raw - if (rendering_raw || edit_mode) { - let delta = - evt.originalEvent.deltaY / 2; - current_contrast = Math.max(current_contrast + delta, -100); - render_image_display(); + if (evt.altKey) { + changeZoom(Math.sign(evt.originalEvent.deltaY)); + } else if ((rendering_raw || edit_mode || (rgb && !display_labels)) + && !evt.originalEvent.shiftKey) { + adjuster.changeContrast(evt.originalEvent.deltaY); + } else if ((rendering_raw || edit_mode || (rgb && !display_labels)) + && evt.originalEvent.shiftKey) { + adjuster.changeBrightness(evt.originalEvent.deltaY); } } // handle pressing mouse button (treats this as the beginning // of click&drag, since clicks are handled by Mode.click) function handle_mousedown(evt) { - if (mode.kind !== Modes.prompt) { - mousedown = true; - mouse_x = evt.offsetX; - mouse_y = evt.offsetY; - // begin drawing - if (edit_mode) { - let img_y = Math.floor(mouse_y/scale); - let img_x = Math.floor(mouse_x/scale); - if (thresholding) { - box_start_x = mouse_x; - box_start_y = mouse_y; - mode.action = "draw_threshold_box"; - } else { - mouse_trace.push([img_y, img_x]); + // TODO: refactor "mousedown + mousemove" into ondrag? + mousedown = true; + if (!spacedown) { + if (mode.kind !== Modes.prompt) { + // begin drawing + if (edit_mode) { + if (!brush.show) { + brush.threshX = imgX; + brush.threshY = imgY; + } else if (mode.kind !== Modes.prompt) { + // not if turning on conv brush + mouse_trace.push([imgY, imgX]); + } } } } } function helper_brush_draw() { - if (mousedown) { - // update mouse_trace - let img_y = Math.floor(mouse_y/scale); - let img_x = Math.floor(mouse_x/scale); - mouse_trace.push([img_y, img_x]); + if (mousedown && !spacedown) { + // update mouse_trace, but not if turning on conv brush + if (mode.kind !== Modes.prompt) { + mouse_trace.push([imgY, imgX]); + } } else { - clear_hidden_ctx(); + brush.clearView(); } - brush.x = mouse_x; - brush.y = mouse_y; - brush.draw(hidden_ctx); + brush.addToView(); } -function helper_box_draw(start_y, start_x, end_y, end_x) { - clear_hidden_ctx(); - hidden_ctx.fillStyle = 'red'; - hidden_ctx.fillRect(start_x, start_y, (end_x - start_x), (end_y - start_y)); +// input will typically be evt.offsetX, evt.offsetY (mouse events) +function updateMousePos(x, y) { + // store raw mouse position, in case of pan without mouse movement + _rawMouseX = x; + _rawMouseY = y; + + // convert to viewing pane position, to check whether to access label underneath + canvasPosX = x - padding; + canvasPosY = y - padding; + + // convert to image indices, to use for actions and getting label + if (inRange(canvasPosX, canvasPosY)) { + imgX = Math.floor((canvasPosX * 100 / (scale * zoom) + sx)); + imgY = Math.floor((canvasPosY * 100 / (scale * zoom) + sy)); + brush.x = imgX; + brush.y = imgY; + // update brush preview + if (edit_mode) { + // brush's canvas is keeping track of the brush + if (brush.show) { + helper_brush_draw(); + } else { + brush.boxView(); + } + render_image_display(); + } + } } // handles mouse movement, whether or not mouse button is held down function handle_mousemove(evt) { - // update displayed info depending on where mouse is - mouse_x = evt.offsetX; - mouse_y = evt.offsetY; - render_info_display(); - - // update brush preview - if (edit_mode) { - // hidden canvas is keeping track of the brush - if (!thresholding) { - helper_brush_draw(); - } else if (thresholding && mode.action === "start_threshold") { - clear_hidden_ctx(); - } else if (thresholding && mode.action === "draw_threshold_box") { - helper_box_draw(box_start_y, box_start_x, mouse_y, mouse_x); - } - render_image_display(); + if (spacedown && mousedown) { + panCanvas( + evt.originalEvent.movementX * 100 / (zoom * scale), + evt.originalEvent.movementY * 100 / (zoom * scale) + ); } + + updateMousePos(evt.offsetX, evt.offsetY); + + render_info_display(); } // handles end of click&drag (different from click()) -function handle_mouseup(evt) { - if (mode.kind !== Modes.prompt) { - mousedown = false; - if (edit_mode) { - if (thresholding) { - mode.handle_threshold(evt); - } else { - //send click&drag coordinates to caliban.py to update annotations - mode.handle_draw(); +function handle_mouseup() { + mousedown = false; + if (!spacedown) { + if (mode.kind !== Modes.prompt) { + if (edit_mode) { + if (!brush.show) { + mode.handle_threshold(); + } else { + //send click&drag coordinates to caliban.py to update annotations + mode.handle_draw(); + } + brush.refreshView(); } - // reset brush preview - clear_hidden_ctx(); - brush.x = evt.offsetX; - brush.y = evt.offsetY; - brush.draw(hidden_ctx); } } } @@ -1031,7 +1211,7 @@ function handle_mouseup(evt) { function prepare_canvas() { // bind click on canvas $('#canvas').click(function(evt) { - if (!edit_mode || mode.kind === Modes.prompt) { + if (!spacedown && (!edit_mode || mode.kind === Modes.prompt)) { mode.click(evt); } }); @@ -1049,20 +1229,27 @@ function prepare_canvas() { // handle brush preview handle_mousemove(evt); }); - // bind mouse button release (end of click&drag) - $('#canvas').mouseup(function(evt) { - handle_mouseup(evt); - }); + // mouse button release (end of click&drag) bound to document, not just canvas // bind keypress window.addEventListener('keydown', function(evt) { mode.handle_key(evt.key); }, false); + window.addEventListener('keydown', function(evt) { + if (evt.key === ' ') { + spacedown = true; + } + }, false); + window.addEventListener('keyup', function(evt) { + if (evt.key === ' ') { + spacedown = false; + } + }, false); } function action(action, info, frame = current_frame) { $.ajax({ - type:'POST', - url:"action/" + project_id + "/" + action + "/" + frame, + type: 'POST', + url: `action/${project_id}/${action}/${frame}`, data: info, success: function (payload) { if (payload.error) { @@ -1071,17 +1258,32 @@ function action(action, info, frame = current_frame) { if (payload.imgs) { // load new value of seg_array // array of arrays, contains annotation data for frame - seg_array = payload.imgs.seg_arr; + if (payload.imgs.hasOwnProperty('seg_arr')) { + seg_array = payload.imgs.seg_arr; + } + + if (payload.imgs.hasOwnProperty('segmented')) { + adjuster.segLoaded = false; + adjuster.segImage.src = payload.imgs.segmented; + } - seg_image.src = payload.imgs.segmented; - raw_image.src = payload.imgs.raw; + if (payload.imgs.hasOwnProperty('raw')) { + adjuster.rawLoaded = false; + adjuster.rawImage.src = payload.imgs.raw; + } } if (payload.tracks) { tracks = payload.tracks; - //update maxLabelsMap when we get new track info + // update maxLabelsMap when we get new track info for (let i = 0; i < Object.keys(tracks).length; i++){ - let key = Object.keys(tracks)[i]; //the keys are strings - maxLabelsMap.set(i, Math.max(... Object.keys(tracks[key]).map(Number))); + let key = Object.keys(tracks)[i]; // the keys are strings + if (Object.keys(tracks[key]).length > 0) { + // use i as key in this map because it is an int, mode.feature is also int + maxLabelsMap.set(i, Math.max(... Object.keys(tracks[key]).map(Number))); + } else { + // if no labels in feature, explicitly set max label to 0 + maxLabelsMap.set(i, 0); + } } } if (payload.tracks || payload.imgs) { @@ -1093,23 +1295,57 @@ function action(action, info, frame = current_frame) { } function start_caliban(filename) { + if (settings.pixel_only && !settings.label_only) { + edit_mode = true; + } else { + edit_mode = false; + } + rgb = settings.rgb; + if (rgb) { + current_highlight = true; + display_labels = false; + } else { + current_highlight = false; + display_labels = true; + } + // disable scrolling from scrolling around on page (it should just control brightness) + document.addEventListener('wheel', function(event) { + event.preventDefault(); + }, {passive: false}); + // disable space and up/down keys from moving around on page + $(document).on('keydown', function(event) { + if (event.key === ' ') { + event.preventDefault(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + } else if (event.key === 'ArrowDown') { + event.preventDefault(); + } + }); + + // resize the canvas every time the window is resized + $(window).resize(function () { + waitForFinalEvent(function() { + mode.clear(); + setCanvasDimensions(rawDimensions); + brush.refreshView(); + }, 500, 'canvasResize'); + }); + + document.addEventListener('mouseup', function() { + handle_mouseup(); + }); + load_file(filename); + + // define image onload cascade behavior, need rawHeight and rawWidth first + adjuster = new ImageAdjuster(width=rawWidth, height=rawHeight, + rgb=rgb, channelMax=channelMax); + brush = new Brush(scale=scale, height=rawHeight, width=rawWidth, pad=padding); + + adjuster.postCompImg.onload = render_image_display; + prepare_canvas(); fetch_and_render_frame(); - update_seg_highlight(); - - brush = { - x: 0, - y: 0, - radius: 1, - color: 'red', - draw: function(ctx) { - ctx.beginPath(); - ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true); - ctx.closePath(); - ctx.fillStyle = this.color; - ctx.fill(); - } - } - hidden_ctx = $('#hidden_canvas').get(0).getContext("2d"); -} \ No newline at end of file + +} diff --git a/browser/static/js/materialize.min.js b/browser/static/js/materialize.min.js new file mode 100644 index 000000000..4ff077d6d --- /dev/null +++ b/browser/static/js/materialize.min.js @@ -0,0 +1,6 @@ +/*! + * Materialize v1.0.0 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +var _get=function t(e,i,n){null===e&&(e=Function.prototype);var s=Object.getOwnPropertyDescriptor(e,i);if(void 0===s){var o=Object.getPrototypeOf(e);return null===o?void 0:t(o,i,n)}if("value"in s)return s.value;var a=s.get;return void 0!==a?a.call(n):void 0},_createClass=function(){function n(t,e){for(var i=0;i/,p=/^\w+$/;function v(t,e){e=e||o;var i=u.test(t)?e.getElementsByClassName(t.slice(1)):p.test(t)?e.getElementsByTagName(t):e.querySelectorAll(t);return i}function f(t){if(!i){var e=(i=o.implementation.createHTMLDocument(null)).createElement("base");e.href=o.location.href,i.head.appendChild(e)}return i.body.innerHTML=t,i.body.childNodes}function m(t){"loading"!==o.readyState?t():o.addEventListener("DOMContentLoaded",t)}function g(t,e){if(!t)return this;if(t.cash&&t!==a)return t;var i,n=t,s=0;if(d(t))n=l.test(t)?o.getElementById(t.slice(1)):c.test(t)?f(t):v(t,e);else if(h(t))return m(t),this;if(!n)return this;if(n.nodeType||n===a)this[0]=n,this.length=1;else for(i=this.length=n.length;ss.right-i||l+e.width>window.innerWidth-i)&&(n.right=!0),(ho-i||h+e.height>window.innerHeight-i)&&(n.bottom=!0),n},M.checkPossibleAlignments=function(t,e,i,n){var s={top:!0,right:!0,bottom:!0,left:!0,spaceOnTop:null,spaceOnRight:null,spaceOnBottom:null,spaceOnLeft:null},o="visible"===getComputedStyle(e).overflow,a=e.getBoundingClientRect(),r=Math.min(a.height,window.innerHeight),l=Math.min(a.width,window.innerWidth),h=t.getBoundingClientRect(),d=e.scrollLeft,u=e.scrollTop,c=i.left-d,p=i.top-u,v=i.top+h.height-u;return s.spaceOnRight=o?window.innerWidth-(h.left+i.width):l-(c+i.width),s.spaceOnRight<0&&(s.left=!1),s.spaceOnLeft=o?h.right-i.width:c-i.width+h.width,s.spaceOnLeft<0&&(s.right=!1),s.spaceOnBottom=o?window.innerHeight-(h.top+i.height+n):r-(p+i.height+n),s.spaceOnBottom<0&&(s.top=!1),s.spaceOnTop=o?h.bottom-(i.height+n):v-(i.height-n),s.spaceOnTop<0&&(s.bottom=!1),s},M.getOverflowParent=function(t){return null==t?null:t===document.body||"visible"!==getComputedStyle(t).overflow?t:M.getOverflowParent(t.parentElement)},M.getIdFromTrigger=function(t){var e=t.getAttribute("data-target");return e||(e=(e=t.getAttribute("href"))?e.slice(1):""),e},M.getDocumentScrollTop=function(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},M.getDocumentScrollLeft=function(){return window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0};var getTime=Date.now||function(){return(new Date).getTime()};M.throttle=function(i,n,s){var o=void 0,a=void 0,r=void 0,l=null,h=0;s||(s={});var d=function(){h=!1===s.leading?0:getTime(),l=null,r=i.apply(o,a),o=a=null};return function(){var t=getTime();h||!1!==s.leading||(h=t);var e=n-(t-h);return o=this,a=arguments,e<=0?(clearTimeout(l),l=null,h=t,r=i.apply(o,a),o=a=null):l||!1===s.trailing||(l=setTimeout(d,e)),r}};var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(t,e,i){if(i.get||i.set)throw new TypeError("ES3 does not support getters and setters.");t!=Array.prototype&&t!=Object.prototype&&(t[e]=i.value)},$jscomp.getGlobal=function(t){return"undefined"!=typeof window&&window===t?t:"undefined"!=typeof global&&null!=global?global:t},$jscomp.global=$jscomp.getGlobal(this),$jscomp.SYMBOL_PREFIX="jscomp_symbol_",$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){},$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)},$jscomp.symbolCounter_=0,$jscomp.Symbol=function(t){return $jscomp.SYMBOL_PREFIX+(t||"")+$jscomp.symbolCounter_++},$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var t=$jscomp.global.Symbol.iterator;t||(t=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator")),"function"!=typeof Array.prototype[t]&&$jscomp.defineProperty(Array.prototype,t,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}}),$jscomp.initSymbolIterator=function(){}},$jscomp.arrayIterator=function(t){var e=0;return $jscomp.iteratorPrototype(function(){return e=k.currentTime)for(var h=0;ht&&(s.duration=e.duration),s.children.push(e)}),s.seek(0),s.reset(),s.autoplay&&s.restart(),s},s},O.random=function(t,e){return Math.floor(Math.random()*(e-t+1))+t},O}(),function(r,l){"use strict";var e={accordion:!0,onOpenStart:void 0,onOpenEnd:void 0,onCloseStart:void 0,onCloseEnd:void 0,inDuration:300,outDuration:300},t=function(t){function s(t,e){_classCallCheck(this,s);var i=_possibleConstructorReturn(this,(s.__proto__||Object.getPrototypeOf(s)).call(this,s,t,e));(i.el.M_Collapsible=i).options=r.extend({},s.defaults,e),i.$headers=i.$el.children("li").children(".collapsible-header"),i.$headers.attr("tabindex",0),i._setupEventHandlers();var n=i.$el.children("li.active").children(".collapsible-body");return i.options.accordion?n.first().css("display","block"):n.css("display","block"),i}return _inherits(s,Component),_createClass(s,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Collapsible=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleCollapsibleClickBound=this._handleCollapsibleClick.bind(this),this._handleCollapsibleKeydownBound=this._handleCollapsibleKeydown.bind(this),this.el.addEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.addEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_removeEventHandlers",value:function(){var e=this;this.el.removeEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.removeEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_handleCollapsibleClick",value:function(t){var e=r(t.target).closest(".collapsible-header");if(t.target&&e.length){var i=e.closest(".collapsible");if(i[0]===this.el){var n=e.closest("li"),s=i.children("li"),o=n[0].classList.contains("active"),a=s.index(n);o?this.close(a):this.open(a)}}}},{key:"_handleCollapsibleKeydown",value:function(t){13===t.keyCode&&this._handleCollapsibleClickBound(t)}},{key:"_animateIn",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css({display:"block",overflow:"hidden",height:0,paddingTop:"",paddingBottom:""});var s=n.css("padding-top"),o=n.css("padding-bottom"),a=n[0].scrollHeight;n.css({paddingTop:0,paddingBottom:0}),l({targets:n[0],height:a,paddingTop:s,paddingBottom:o,duration:this.options.inDuration,easing:"easeInOutCubic",complete:function(t){n.css({overflow:"",paddingTop:"",paddingBottom:"",height:""}),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,i[0])}})}}},{key:"_animateOut",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css("overflow","hidden"),l({targets:n[0],height:0,paddingTop:0,paddingBottom:0,duration:this.options.outDuration,easing:"easeInOutCubic",complete:function(){n.css({height:"",overflow:"",padding:"",display:""}),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,i[0])}})}}},{key:"open",value:function(t){var i=this,e=this.$el.children("li").eq(t);if(e.length&&!e[0].classList.contains("active")){if("function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,e[0]),this.options.accordion){var n=this.$el.children("li");this.$el.children("li.active").each(function(t){var e=n.index(r(t));i.close(e)})}e[0].classList.add("active"),this._animateIn(t)}}},{key:"close",value:function(t){var e=this.$el.children("li").eq(t);e.length&&e[0].classList.contains("active")&&("function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,e[0]),e[0].classList.remove("active"),this._animateOut(t))}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Collapsible}},{key:"defaults",get:function(){return e}}]),s}();M.Collapsible=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"collapsible","M_Collapsible")}(cash,M.anime),function(h,i){"use strict";var e={alignment:"left",autoFocus:!0,constrainWidth:!0,container:null,coverTrigger:!0,closeOnClick:!0,hover:!1,inDuration:150,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onItemClick:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return i.el.M_Dropdown=i,n._dropdowns.push(i),i.id=M.getIdFromTrigger(t),i.dropdownEl=document.getElementById(i.id),i.$dropdownEl=h(i.dropdownEl),i.options=h.extend({},n.defaults,e),i.isOpen=!1,i.isScrollable=!1,i.isTouchMoving=!1,i.focusedIndex=-1,i.filterQuery=[],i.options.container?h(i.options.container).append(i.dropdownEl):i.$el.after(i.dropdownEl),i._makeDropdownFocusable(),i._resetFilterQueryBound=i._resetFilterQuery.bind(i),i._handleDocumentClickBound=i._handleDocumentClick.bind(i),i._handleDocumentTouchmoveBound=i._handleDocumentTouchmove.bind(i),i._handleDropdownClickBound=i._handleDropdownClick.bind(i),i._handleDropdownKeydownBound=i._handleDropdownKeydown.bind(i),i._handleTriggerKeydownBound=i._handleTriggerKeydown.bind(i),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._resetDropdownStyles(),this._removeEventHandlers(),n._dropdowns.splice(n._dropdowns.indexOf(this),1),this.el.M_Dropdown=void 0}},{key:"_setupEventHandlers",value:function(){this.el.addEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.addEventListener("click",this._handleDropdownClickBound),this.options.hover?(this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.addEventListener("mouseleave",this._handleMouseLeaveBound)):(this._handleClickBound=this._handleClick.bind(this),this.el.addEventListener("click",this._handleClickBound))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.removeEventListener("click",this._handleDropdownClickBound),this.options.hover?(this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.removeEventListener("mouseleave",this._handleMouseLeaveBound)):this.el.removeEventListener("click",this._handleClickBound)}},{key:"_setupTemporaryEventHandlers",value:function(){document.body.addEventListener("click",this._handleDocumentClickBound,!0),document.body.addEventListener("touchend",this._handleDocumentClickBound),document.body.addEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.addEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_removeTemporaryEventHandlers",value:function(){document.body.removeEventListener("click",this._handleDocumentClickBound,!0),document.body.removeEventListener("touchend",this._handleDocumentClickBound),document.body.removeEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.removeEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_handleClick",value:function(t){t.preventDefault(),this.open()}},{key:"_handleMouseEnter",value:function(){this.open()}},{key:"_handleMouseLeave",value:function(t){var e=t.toElement||t.relatedTarget,i=!!h(e).closest(".dropdown-content").length,n=!1,s=h(e).closest(".dropdown-trigger");s.length&&s[0].M_Dropdown&&s[0].M_Dropdown.isOpen&&(n=!0),n||i||this.close()}},{key:"_handleDocumentClick",value:function(t){var e=this,i=h(t.target);this.options.closeOnClick&&i.closest(".dropdown-content").length&&!this.isTouchMoving?setTimeout(function(){e.close()},0):!i.closest(".dropdown-trigger").length&&i.closest(".dropdown-content").length||setTimeout(function(){e.close()},0),this.isTouchMoving=!1}},{key:"_handleTriggerKeydown",value:function(t){t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ENTER||this.isOpen||(t.preventDefault(),this.open())}},{key:"_handleDocumentTouchmove",value:function(t){h(t.target).closest(".dropdown-content").length&&(this.isTouchMoving=!0)}},{key:"_handleDropdownClick",value:function(t){if("function"==typeof this.options.onItemClick){var e=h(t.target).closest("li")[0];this.options.onItemClick.call(this,e)}}},{key:"_handleDropdownKeydown",value:function(t){if(t.which===M.keys.TAB)t.preventDefault(),this.close();else if(t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||!this.isOpen)if(t.which===M.keys.ENTER&&this.isOpen){var e=this.dropdownEl.children[this.focusedIndex],i=h(e).find("a, button").first();i.length?i[0].click():e&&e.click()}else t.which===M.keys.ESC&&this.isOpen&&(t.preventDefault(),this.close());else{t.preventDefault();var n=t.which===M.keys.ARROW_DOWN?1:-1,s=this.focusedIndex,o=!1;do{if(s+=n,this.dropdownEl.children[s]&&-1!==this.dropdownEl.children[s].tabIndex){o=!0;break}}while(sl.spaceOnBottom?(h="bottom",i+=l.spaceOnTop,o-=l.spaceOnTop):i+=l.spaceOnBottom)),!l[d]){var u="left"===d?"right":"left";l[u]?d=u:l.spaceOnLeft>l.spaceOnRight?(d="right",n+=l.spaceOnLeft,s-=l.spaceOnLeft):(d="left",n+=l.spaceOnRight)}return"bottom"===h&&(o=o-e.height+(this.options.coverTrigger?t.height:0)),"right"===d&&(s=s-e.width+t.width),{x:s,y:o,verticalAlignment:h,horizontalAlignment:d,height:i,width:n}}},{key:"_animateIn",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:[0,1],easing:"easeOutQuad"},scaleX:[.3,1],scaleY:[.3,1],duration:this.options.inDuration,easing:"easeOutQuint",complete:function(t){e.options.autoFocus&&e.dropdownEl.focus(),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,e.el)}})}},{key:"_animateOut",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:0,easing:"easeOutQuint"},scaleX:.3,scaleY:.3,duration:this.options.outDuration,easing:"easeOutQuint",complete:function(t){e._resetDropdownStyles(),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,e.el)}})}},{key:"_placeDropdown",value:function(){var t=this.options.constrainWidth?this.el.getBoundingClientRect().width:this.dropdownEl.getBoundingClientRect().width;this.dropdownEl.style.width=t+"px";var e=this._getDropdownPosition();this.dropdownEl.style.left=e.x+"px",this.dropdownEl.style.top=e.y+"px",this.dropdownEl.style.height=e.height+"px",this.dropdownEl.style.width=e.width+"px",this.dropdownEl.style.transformOrigin=("left"===e.horizontalAlignment?"0":"100%")+" "+("top"===e.verticalAlignment?"0":"100%")}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._resetDropdownStyles(),this.dropdownEl.style.display="block",this._placeDropdown(),this._animateIn(),this._setupTemporaryEventHandlers())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.focusedIndex=-1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._animateOut(),this._removeTemporaryEventHandlers(),this.options.autoFocus&&this.el.focus())}},{key:"recalculateDimensions",value:function(){this.isOpen&&(this.$dropdownEl.css({width:"",height:"",left:"",top:"","transform-origin":""}),this._placeDropdown())}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Dropdown}},{key:"defaults",get:function(){return e}}]),n}();t._dropdowns=[],M.Dropdown=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"dropdown","M_Dropdown")}(cash,M.anime),function(s,i){"use strict";var e={opacity:.5,inDuration:250,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0,dismissible:!0,startingTop:"4%",endingTop:"10%"},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Modal=i).options=s.extend({},n.defaults,e),i.isOpen=!1,i.id=i.$el.attr("id"),i._openingTrigger=void 0,i.$overlay=s(''),i.el.tabIndex=0,i._nthModalOpened=0,n._count++,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._count--,this._removeEventHandlers(),this.el.removeAttribute("style"),this.$overlay.remove(),this.el.M_Modal=void 0}},{key:"_setupEventHandlers",value:function(){this._handleOverlayClickBound=this._handleOverlayClick.bind(this),this._handleModalCloseClickBound=this._handleModalCloseClick.bind(this),1===n._count&&document.body.addEventListener("click",this._handleTriggerClick),this.$overlay[0].addEventListener("click",this._handleOverlayClickBound),this.el.addEventListener("click",this._handleModalCloseClickBound)}},{key:"_removeEventHandlers",value:function(){0===n._count&&document.body.removeEventListener("click",this._handleTriggerClick),this.$overlay[0].removeEventListener("click",this._handleOverlayClickBound),this.el.removeEventListener("click",this._handleModalCloseClickBound)}},{key:"_handleTriggerClick",value:function(t){var e=s(t.target).closest(".modal-trigger");if(e.length){var i=M.getIdFromTrigger(e[0]),n=document.getElementById(i).M_Modal;n&&n.open(e),t.preventDefault()}}},{key:"_handleOverlayClick",value:function(){this.options.dismissible&&this.close()}},{key:"_handleModalCloseClick",value:function(t){s(t.target).closest(".modal-close").length&&this.close()}},{key:"_handleKeydown",value:function(t){27===t.keyCode&&this.options.dismissible&&this.close()}},{key:"_handleFocus",value:function(t){this.el.contains(t.target)||this._nthModalOpened!==n._modalsOpen||this.el.focus()}},{key:"_animateIn",value:function(){var t=this;s.extend(this.el.style,{display:"block",opacity:0}),s.extend(this.$overlay[0].style,{display:"block",opacity:0}),i({targets:this.$overlay[0],opacity:this.options.opacity,duration:this.options.inDuration,easing:"easeOutQuad"});var e={targets:this.el,duration:this.options.inDuration,easing:"easeOutCubic",complete:function(){"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el,t._openingTrigger)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:0,opacity:1}):s.extend(e,{top:[this.options.startingTop,this.options.endingTop],opacity:1,scaleX:[.8,1],scaleY:[.8,1]}),i(e)}},{key:"_animateOut",value:function(){var t=this;i({targets:this.$overlay[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuart"});var e={targets:this.el,duration:this.options.outDuration,easing:"easeOutCubic",complete:function(){t.el.style.display="none",t.$overlay.remove(),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:"-100%",opacity:0}):s.extend(e,{top:[this.options.endingTop,this.options.startingTop],opacity:0,scaleX:.8,scaleY:.8}),i(e)}},{key:"open",value:function(t){if(!this.isOpen)return this.isOpen=!0,n._modalsOpen++,this._nthModalOpened=n._modalsOpen,this.$overlay[0].style.zIndex=1e3+2*n._modalsOpen,this.el.style.zIndex=1e3+2*n._modalsOpen+1,this._openingTrigger=t?t[0]:void 0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el,this._openingTrigger),this.options.preventScrolling&&(document.body.style.overflow="hidden"),this.el.classList.add("open"),this.el.insertAdjacentElement("afterend",this.$overlay[0]),this.options.dismissible&&(this._handleKeydownBound=this._handleKeydown.bind(this),this._handleFocusBound=this._handleFocus.bind(this),document.addEventListener("keydown",this._handleKeydownBound),document.addEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateIn(),this.el.focus(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,n._modalsOpen--,this._nthModalOpened=0,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this.el.classList.remove("open"),0===n._modalsOpen&&(document.body.style.overflow=""),this.options.dismissible&&(document.removeEventListener("keydown",this._handleKeydownBound),document.removeEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateOut(),this}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Modal}},{key:"defaults",get:function(){return e}}]),n}();t._modalsOpen=0,t._count=0,M.Modal=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"modal","M_Modal")}(cash,M.anime),function(o,a){"use strict";var e={inDuration:275,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Materialbox=i).options=o.extend({},n.defaults,e),i.overlayActive=!1,i.doneAnimating=!0,i.placeholder=o("
").addClass("material-placeholder"),i.originalWidth=0,i.originalHeight=0,i.originInlineStyles=i.$el.attr("style"),i.caption=i.el.getAttribute("data-caption")||"",i.$el.before(i.placeholder),i.placeholder.append(i.$el),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Materialbox=void 0,o(this.placeholder).after(this.el).remove(),this.$el.removeAttr("style")}},{key:"_setupEventHandlers",value:function(){this._handleMaterialboxClickBound=this._handleMaterialboxClick.bind(this),this.el.addEventListener("click",this._handleMaterialboxClickBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleMaterialboxClickBound)}},{key:"_handleMaterialboxClick",value:function(t){!1===this.doneAnimating||this.overlayActive&&this.doneAnimating?this.close():this.open()}},{key:"_handleWindowScroll",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowResize",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowEscape",value:function(t){27===t.keyCode&&this.doneAnimating&&this.overlayActive&&this.close()}},{key:"_makeAncestorsOverflowVisible",value:function(){this.ancestorsChanged=o();for(var t=this.placeholder[0].parentNode;null!==t&&!o(t).is(document);){var e=o(t);"visible"!==e.css("overflow")&&(e.css("overflow","visible"),void 0===this.ancestorsChanged?this.ancestorsChanged=e:this.ancestorsChanged=this.ancestorsChanged.add(e)),t=t.parentNode}}},{key:"_animateImageIn",value:function(){var t=this,e={targets:this.el,height:[this.originalHeight,this.newHeight],width:[this.originalWidth,this.newWidth],left:M.getDocumentScrollLeft()+this.windowWidth/2-this.placeholder.offset().left-this.newWidth/2,top:M.getDocumentScrollTop()+this.windowHeight/2-this.placeholder.offset().top-this.newHeight/2,duration:this.options.inDuration,easing:"easeOutQuad",complete:function(){t.doneAnimating=!0,"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el)}};this.maxWidth=this.$el.css("max-width"),this.maxHeight=this.$el.css("max-height"),"none"!==this.maxWidth&&(e.maxWidth=this.newWidth),"none"!==this.maxHeight&&(e.maxHeight=this.newHeight),a(e)}},{key:"_animateImageOut",value:function(){var t=this,e={targets:this.el,width:this.originalWidth,height:this.originalHeight,left:0,top:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.placeholder.css({height:"",width:"",position:"",top:"",left:""}),t.attrWidth&&t.$el.attr("width",t.attrWidth),t.attrHeight&&t.$el.attr("height",t.attrHeight),t.$el.removeAttr("style"),t.originInlineStyles&&t.$el.attr("style",t.originInlineStyles),t.$el.removeClass("active"),t.doneAnimating=!0,t.ancestorsChanged.length&&t.ancestorsChanged.css("overflow",""),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};a(e)}},{key:"_updateVars",value:function(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight,this.caption=this.el.getAttribute("data-caption")||""}},{key:"open",value:function(){var t=this;this._updateVars(),this.originalWidth=this.el.getBoundingClientRect().width,this.originalHeight=this.el.getBoundingClientRect().height,this.doneAnimating=!1,this.$el.addClass("active"),this.overlayActive=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this.placeholder.css({width:this.placeholder[0].getBoundingClientRect().width+"px",height:this.placeholder[0].getBoundingClientRect().height+"px",position:"relative",top:0,left:0}),this._makeAncestorsOverflowVisible(),this.$el.css({position:"absolute","z-index":1e3,"will-change":"left, top, width, height"}),this.attrWidth=this.$el.attr("width"),this.attrHeight=this.$el.attr("height"),this.attrWidth&&(this.$el.css("width",this.attrWidth+"px"),this.$el.removeAttr("width")),this.attrHeight&&(this.$el.css("width",this.attrHeight+"px"),this.$el.removeAttr("height")),this.$overlay=o('
').css({opacity:0}).one("click",function(){t.doneAnimating&&t.close()}),this.$el.before(this.$overlay);var e=this.$overlay[0].getBoundingClientRect();this.$overlay.css({width:this.windowWidth+"px",height:this.windowHeight+"px",left:-1*e.left+"px",top:-1*e.top+"px"}),a.remove(this.el),a.remove(this.$overlay[0]),a({targets:this.$overlay[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}),""!==this.caption&&(this.$photocaption&&a.remove(this.$photoCaption[0]),this.$photoCaption=o('
'),this.$photoCaption.text(this.caption),o("body").append(this.$photoCaption),this.$photoCaption.css({display:"inline"}),a({targets:this.$photoCaption[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}));var i=0,n=this.originalWidth/this.windowWidth,s=this.originalHeight/this.windowHeight;this.newWidth=0,this.newHeight=0,si.options.responsiveThreshold,i.$img=i.$el.find("img").first(),i.$img.each(function(){this.complete&&s(this).trigger("load")}),i._updateParallax(),i._setupEventHandlers(),i._setupStyles(),n._parallaxes.push(i),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._parallaxes.splice(n._parallaxes.indexOf(this),1),this.$img[0].style.transform="",this._removeEventHandlers(),this.$el[0].M_Parallax=void 0}},{key:"_setupEventHandlers",value:function(){this._handleImageLoadBound=this._handleImageLoad.bind(this),this.$img[0].addEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(n._handleScrollThrottled=M.throttle(n._handleScroll,5),window.addEventListener("scroll",n._handleScrollThrottled),n._handleWindowResizeThrottled=M.throttle(n._handleWindowResize,5),window.addEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_removeEventHandlers",value:function(){this.$img[0].removeEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(window.removeEventListener("scroll",n._handleScrollThrottled),window.removeEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_setupStyles",value:function(){this.$img[0].style.opacity=1}},{key:"_handleImageLoad",value:function(){this._updateParallax()}},{key:"_updateParallax",value:function(){var t=0e.options.responsiveThreshold}}},{key:"defaults",get:function(){return e}}]),n}();t._parallaxes=[],M.Parallax=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"parallax","M_Parallax")}(cash),function(a,s){"use strict";var e={duration:300,onShow:null,swipeable:!1,responsiveThreshold:1/0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tabs=i).options=a.extend({},n.defaults,e),i.$tabLinks=i.$el.children("li.tab").children("a"),i.index=0,i._setupActiveTabLink(),i.options.swipeable?i._setupSwipeableTabs():i._setupNormalTabs(),i._setTabsAndTabWidth(),i._createIndicator(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._indicator.parentNode.removeChild(this._indicator),this.options.swipeable?this._teardownSwipeableTabs():this._teardownNormalTabs(),this.$el[0].M_Tabs=void 0}},{key:"_setupEventHandlers",value:function(){this._handleWindowResizeBound=this._handleWindowResize.bind(this),window.addEventListener("resize",this._handleWindowResizeBound),this._handleTabClickBound=this._handleTabClick.bind(this),this.el.addEventListener("click",this._handleTabClickBound)}},{key:"_removeEventHandlers",value:function(){window.removeEventListener("resize",this._handleWindowResizeBound),this.el.removeEventListener("click",this._handleTabClickBound)}},{key:"_handleWindowResize",value:function(){this._setTabsAndTabWidth(),0!==this.tabWidth&&0!==this.tabsWidth&&(this._indicator.style.left=this._calcLeftPos(this.$activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this.$activeTabLink)+"px")}},{key:"_handleTabClick",value:function(t){var e=this,i=a(t.target).closest("li.tab"),n=a(t.target).closest("a");if(n.length&&n.parent().hasClass("tab"))if(i.hasClass("disabled"))t.preventDefault();else if(!n.attr("target")){this.$activeTabLink.removeClass("active");var s=this.$content;this.$activeTabLink=n,this.$content=a(M.escapeHash(n[0].hash)),this.$tabLinks=this.$el.children("li.tab").children("a"),this.$activeTabLink.addClass("active");var o=this.index;this.index=Math.max(this.$tabLinks.index(n),0),this.options.swipeable?this._tabsCarousel&&this._tabsCarousel.set(this.index,function(){"function"==typeof e.options.onShow&&e.options.onShow.call(e,e.$content[0])}):this.$content.length&&(this.$content[0].style.display="block",this.$content.addClass("active"),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this.$content[0]),s.length&&!s.is(this.$content)&&(s[0].style.display="none",s.removeClass("active"))),this._setTabsAndTabWidth(),this._animateIndicator(o),t.preventDefault()}}},{key:"_createIndicator",value:function(){var t=this,e=document.createElement("li");e.classList.add("indicator"),this.el.appendChild(e),this._indicator=e,setTimeout(function(){t._indicator.style.left=t._calcLeftPos(t.$activeTabLink)+"px",t._indicator.style.right=t._calcRightPos(t.$activeTabLink)+"px"},0)}},{key:"_setupActiveTabLink",value:function(){this.$activeTabLink=a(this.$tabLinks.filter('[href="'+location.hash+'"]')),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a.active").first()),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a").first()),this.$tabLinks.removeClass("active"),this.$activeTabLink[0].classList.add("active"),this.index=Math.max(this.$tabLinks.index(this.$activeTabLink),0),this.$activeTabLink.length&&(this.$content=a(M.escapeHash(this.$activeTabLink[0].hash)),this.$content.addClass("active"))}},{key:"_setupSwipeableTabs",value:function(){var i=this;window.innerWidth>this.options.responsiveThreshold&&(this.options.swipeable=!1);var n=a();this.$tabLinks.each(function(t){var e=a(M.escapeHash(t.hash));e.addClass("carousel-item"),n=n.add(e)});var t=a('');n.first().before(t),t.append(n),n[0].style.display="";var e=this.$activeTabLink.closest(".tab").index();this._tabsCarousel=M.Carousel.init(t[0],{fullWidth:!0,noWrap:!0,onCycleTo:function(t){var e=i.index;i.index=a(t).index(),i.$activeTabLink.removeClass("active"),i.$activeTabLink=i.$tabLinks.eq(i.index),i.$activeTabLink.addClass("active"),i._animateIndicator(e),"function"==typeof i.options.onShow&&i.options.onShow.call(i,i.$content[0])}}),this._tabsCarousel.set(e)}},{key:"_teardownSwipeableTabs",value:function(){var t=this._tabsCarousel.$el;this._tabsCarousel.destroy(),t.after(t.children()),t.remove()}},{key:"_setupNormalTabs",value:function(){this.$tabLinks.not(this.$activeTabLink).each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="none")}})}},{key:"_teardownNormalTabs",value:function(){this.$tabLinks.each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="")}})}},{key:"_setTabsAndTabWidth",value:function(){this.tabsWidth=this.$el.width(),this.tabWidth=Math.max(this.tabsWidth,this.el.scrollWidth)/this.$tabLinks.length}},{key:"_calcRightPos",value:function(t){return Math.ceil(this.tabsWidth-t.position().left-t[0].getBoundingClientRect().width)}},{key:"_calcLeftPos",value:function(t){return Math.floor(t.position().left)}},{key:"updateTabIndicator",value:function(){this._setTabsAndTabWidth(),this._animateIndicator(this.index)}},{key:"_animateIndicator",value:function(t){var e=0,i=0;0<=this.index-t?e=90:i=90;var n={targets:this._indicator,left:{value:this._calcLeftPos(this.$activeTabLink),delay:e},right:{value:this._calcRightPos(this.$activeTabLink),delay:i},duration:this.options.duration,easing:"easeOutQuad"};s.remove(this._indicator),s(n)}},{key:"select",value:function(t){var e=this.$tabLinks.filter('[href="#'+t+'"]');e.length&&e.trigger("click")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tabs}},{key:"defaults",get:function(){return e}}]),n}();M.Tabs=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tabs","M_Tabs")}(cash,M.anime),function(d,e){"use strict";var i={exitDelay:200,enterDelay:0,html:null,margin:5,inDuration:250,outDuration:200,position:"bottom",transitionMovement:10},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tooltip=i).options=d.extend({},n.defaults,e),i.isOpen=!1,i.isHovered=!1,i.isFocused=!1,i._appendTooltipEl(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){d(this.tooltipEl).remove(),this._removeEventHandlers(),this.el.M_Tooltip=void 0}},{key:"_appendTooltipEl",value:function(){var t=document.createElement("div");t.classList.add("material-tooltip"),this.tooltipEl=t;var e=document.createElement("div");e.classList.add("tooltip-content"),e.innerHTML=this.options.html,t.appendChild(e),document.body.appendChild(t)}},{key:"_updateTooltipContent",value:function(){this.tooltipEl.querySelector(".tooltip-content").innerHTML=this.options.html}},{key:"_setupEventHandlers",value:function(){this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this._handleFocusBound=this._handleFocus.bind(this),this._handleBlurBound=this._handleBlur.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.el.addEventListener("focus",this._handleFocusBound,!0),this.el.addEventListener("blur",this._handleBlurBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.el.removeEventListener("focus",this._handleFocusBound,!0),this.el.removeEventListener("blur",this._handleBlurBound,!0)}},{key:"open",value:function(t){this.isOpen||(t=void 0===t||void 0,this.isOpen=!0,this.options=d.extend({},this.options,this._getAttributeOptions()),this._updateTooltipContent(),this._setEnterDelayTimeout(t))}},{key:"close",value:function(){this.isOpen&&(this.isHovered=!1,this.isFocused=!1,this.isOpen=!1,this._setExitDelayTimeout())}},{key:"_setExitDelayTimeout",value:function(){var t=this;clearTimeout(this._exitDelayTimeout),this._exitDelayTimeout=setTimeout(function(){t.isHovered||t.isFocused||t._animateOut()},this.options.exitDelay)}},{key:"_setEnterDelayTimeout",value:function(t){var e=this;clearTimeout(this._enterDelayTimeout),this._enterDelayTimeout=setTimeout(function(){(e.isHovered||e.isFocused||t)&&e._animateIn()},this.options.enterDelay)}},{key:"_positionTooltip",value:function(){var t,e=this.el,i=this.tooltipEl,n=e.offsetHeight,s=e.offsetWidth,o=i.offsetHeight,a=i.offsetWidth,r=this.options.margin,l=void 0,h=void 0;this.xMovement=0,this.yMovement=0,l=e.getBoundingClientRect().top+M.getDocumentScrollTop(),h=e.getBoundingClientRect().left+M.getDocumentScrollLeft(),"top"===this.options.position?(l+=-o-r,h+=s/2-a/2,this.yMovement=-this.options.transitionMovement):"right"===this.options.position?(l+=n/2-o/2,h+=s+r,this.xMovement=this.options.transitionMovement):"left"===this.options.position?(l+=n/2-o/2,h+=-a-r,this.xMovement=-this.options.transitionMovement):(l+=n+r,h+=s/2-a/2,this.yMovement=this.options.transitionMovement),t=this._repositionWithinScreen(h,l,a,o),d(i).css({top:t.y+"px",left:t.x+"px"})}},{key:"_repositionWithinScreen",value:function(t,e,i,n){var s=M.getDocumentScrollLeft(),o=M.getDocumentScrollTop(),a=t-s,r=e-o,l={left:a,top:r,width:i,height:n},h=this.options.margin+this.options.transitionMovement,d=M.checkWithinContainer(document.body,l,h);return d.left?a=h:d.right&&(a-=a+i-window.innerWidth),d.top?r=h:d.bottom&&(r-=r+n-window.innerHeight),{x:a+s,y:r+o}}},{key:"_animateIn",value:function(){this._positionTooltip(),this.tooltipEl.style.visibility="visible",e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:1,translateX:this.xMovement,translateY:this.yMovement,duration:this.options.inDuration,easing:"easeOutCubic"})}},{key:"_animateOut",value:function(){e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:0,translateX:0,translateY:0,duration:this.options.outDuration,easing:"easeOutCubic"})}},{key:"_handleMouseEnter",value:function(){this.isHovered=!0,this.isFocused=!1,this.open(!1)}},{key:"_handleMouseLeave",value:function(){this.isHovered=!1,this.isFocused=!1,this.close()}},{key:"_handleFocus",value:function(){M.tabPressed&&(this.isFocused=!0,this.open(!1))}},{key:"_handleBlur",value:function(){this.isFocused=!1,this.close()}},{key:"_getAttributeOptions",value:function(){var t={},e=this.el.getAttribute("data-tooltip"),i=this.el.getAttribute("data-position");return e&&(t.html=e),i&&(t.position=i),t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tooltip}},{key:"defaults",get:function(){return i}}]),n}();M.Tooltip=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tooltip","M_Tooltip")}(cash,M.anime),function(i){"use strict";var t=t||{},e=document.querySelectorAll.bind(document);function m(t){var e="";for(var i in t)t.hasOwnProperty(i)&&(e+=i+":"+t[i]+";");return e}var g={duration:750,show:function(t,e){if(2===t.button)return!1;var i=e||this,n=document.createElement("div");n.className="waves-ripple",i.appendChild(n);var s,o,a,r,l,h,d,u=(h={top:0,left:0},d=(s=i)&&s.ownerDocument,o=d.documentElement,void 0!==s.getBoundingClientRect&&(h=s.getBoundingClientRect()),a=null!==(l=r=d)&&l===l.window?r:9===r.nodeType&&r.defaultView,{top:h.top+a.pageYOffset-o.clientTop,left:h.left+a.pageXOffset-o.clientLeft}),c=t.pageY-u.top,p=t.pageX-u.left,v="scale("+i.clientWidth/100*10+")";"touches"in t&&(c=t.touches[0].pageY-u.top,p=t.touches[0].pageX-u.left),n.setAttribute("data-hold",Date.now()),n.setAttribute("data-scale",v),n.setAttribute("data-x",p),n.setAttribute("data-y",c);var f={top:c+"px",left:p+"px"};n.className=n.className+" waves-notransition",n.setAttribute("style",m(f)),n.className=n.className.replace("waves-notransition",""),f["-webkit-transform"]=v,f["-moz-transform"]=v,f["-ms-transform"]=v,f["-o-transform"]=v,f.transform=v,f.opacity="1",f["-webkit-transition-duration"]=g.duration+"ms",f["-moz-transition-duration"]=g.duration+"ms",f["-o-transition-duration"]=g.duration+"ms",f["transition-duration"]=g.duration+"ms",f["-webkit-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-moz-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-o-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",n.setAttribute("style",m(f))},hide:function(t){l.touchup(t);var e=this,i=(e.clientWidth,null),n=e.getElementsByClassName("waves-ripple");if(!(0i||1"+o+""+a+""+r+""),i.length&&e.prepend(i)}},{key:"_resetCurrentElement",value:function(){this.activeIndex=-1,this.$active.removeClass("active")}},{key:"_resetAutocomplete",value:function(){h(this.container).empty(),this._resetCurrentElement(),this.oldVal=null,this.isOpen=!1,this._mousedown=!1}},{key:"selectOption",value:function(t){var e=t.text().trim();this.el.value=e,this.$el.trigger("change"),this._resetAutocomplete(),this.close(),"function"==typeof this.options.onAutocomplete&&this.options.onAutocomplete.call(this,e)}},{key:"_renderDropdown",value:function(t,i){var n=this;this._resetAutocomplete();var e=[];for(var s in t)if(t.hasOwnProperty(s)&&-1!==s.toLowerCase().indexOf(i)){if(this.count>=this.options.limit)break;var o={data:t[s],key:s};e.push(o),this.count++}if(this.options.sortFunction){e.sort(function(t,e){return n.options.sortFunction(t.key.toLowerCase(),e.key.toLowerCase(),i.toLowerCase())})}for(var a=0;a");r.data?l.append(''+r.key+""):l.append(""+r.key+""),h(this.container).append(l),this._highlight(i,l)}}},{key:"open",value:function(){var t=this.el.value.toLowerCase();this._resetAutocomplete(),t.length>=this.options.minLength&&(this.isOpen=!0,this._renderDropdown(this.options.data,t)),this.dropdown.isOpen?this.dropdown.recalculateDimensions():this.dropdown.open()}},{key:"close",value:function(){this.dropdown.close()}},{key:"updateData",value:function(t){var e=this.el.value.toLowerCase();this.options.data=t,this.isOpen&&this._renderDropdown(t,e)}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Autocomplete}},{key:"defaults",get:function(){return e}}]),s}();t._keydown=!1,M.Autocomplete=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"autocomplete","M_Autocomplete")}(cash),function(d){M.updateTextFields=function(){d("input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea").each(function(t,e){var i=d(this);0'),d("body").append(e));var i=t.css("font-family"),n=t.css("font-size"),s=t.css("line-height"),o=t.css("padding-top"),a=t.css("padding-right"),r=t.css("padding-bottom"),l=t.css("padding-left");n&&e.css("font-size",n),i&&e.css("font-family",i),s&&e.css("line-height",s),o&&e.css("padding-top",o),a&&e.css("padding-right",a),r&&e.css("padding-bottom",r),l&&e.css("padding-left",l),t.data("original-height")||t.data("original-height",t.height()),"off"===t.attr("wrap")&&e.css("overflow-wrap","normal").css("white-space","pre"),e.text(t[0].value+"\n");var h=e.html().replace(/\n/g,"
");e.html(h),0'),this.$slides.each(function(t,e){var i=s('
  • ');n.$indicators.append(i[0])}),this.$el.append(this.$indicators[0]),this.$indicators=this.$indicators.children("li.indicator-item"))}},{key:"_removeIndicators",value:function(){this.$el.find("ul.indicators").remove()}},{key:"set",value:function(t){var e=this;if(t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.activeIndex!=t){this.$active=this.$slides.eq(this.activeIndex);var i=this.$active.find(".caption");this.$active.removeClass("active"),o({targets:this.$active[0],opacity:0,duration:this.options.duration,easing:"easeOutQuad",complete:function(){e.$slides.not(".active").each(function(t){o({targets:t,opacity:0,translateX:0,translateY:0,duration:0,easing:"easeOutQuad"})})}}),this._animateCaptionIn(i[0],this.options.duration),this.options.indicators&&(this.$indicators.eq(this.activeIndex).removeClass("active"),this.$indicators.eq(t).addClass("active")),o({targets:this.$slides.eq(t)[0],opacity:1,duration:this.options.duration,easing:"easeOutQuad"}),o({targets:this.$slides.eq(t).find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:this.options.duration,delay:this.options.duration,easing:"easeOutQuad"}),this.$slides.eq(t).addClass("active"),this.activeIndex=t,this.start()}}},{key:"pause",value:function(){clearInterval(this.interval)}},{key:"start",value:function(){clearInterval(this.interval),this.interval=setInterval(this._handleIntervalBound,this.options.duration+this.options.interval)}},{key:"next",value:function(){var t=this.activeIndex+1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}},{key:"prev",value:function(){var t=this.activeIndex-1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Slider}},{key:"defaults",get:function(){return e}}]),n}();M.Slider=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"slider","M_Slider")}(cash,M.anime),function(n,s){n(document).on("click",".card",function(t){if(n(this).children(".card-reveal").length){var i=n(t.target).closest(".card");void 0===i.data("initialOverflow")&&i.data("initialOverflow",void 0===i.css("overflow")?"":i.css("overflow"));var e=n(this).find(".card-reveal");n(t.target).is(n(".card-reveal .card-title"))||n(t.target).is(n(".card-reveal .card-title i"))?s({targets:e[0],translateY:0,duration:225,easing:"easeInOutQuad",complete:function(t){var e=t.animatables[0].target;n(e).css({display:"none"}),i.css("overflow",i.data("initialOverflow"))}}):(n(t.target).is(n(".card .activator"))||n(t.target).is(n(".card .activator i")))&&(i.css("overflow","hidden"),e.css({display:"block"}),s({targets:e[0],translateY:"-100%",duration:300,easing:"easeInOutQuad"}))}})}(cash,M.anime),function(h){"use strict";var e={data:[],placeholder:"",secondaryPlaceholder:"",autocompleteOptions:{},limit:1/0,onChipAdd:null,onChipSelect:null,onChipDelete:null},t=function(t){function l(t,e){_classCallCheck(this,l);var i=_possibleConstructorReturn(this,(l.__proto__||Object.getPrototypeOf(l)).call(this,l,t,e));return(i.el.M_Chips=i).options=h.extend({},l.defaults,e),i.$el.addClass("chips input-field"),i.chipsData=[],i.$chips=h(),i._setupInput(),i.hasAutocomplete=0"),this.$el.append(this.$input)),this.$input.addClass("input")}},{key:"_setupLabel",value:function(){this.$label=this.$el.find("label"),this.$label.length&&this.$label.setAttribute("for",this.$input.attr("id"))}},{key:"_setPlaceholder",value:function(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?h(this.$input).prop("placeholder",this.options.placeholder):(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&h(this.$input).prop("placeholder",this.options.secondaryPlaceholder)}},{key:"_isValid",value:function(t){if(t.hasOwnProperty("tag")&&""!==t.tag){for(var e=!1,i=0;i=this.options.limit)){var e=this._renderChip(t);this.$chips.add(e),this.chipsData.push(t),h(this.$input).before(e),this._setPlaceholder(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd.call(this,this.$el,e)}}},{key:"deleteChip",value:function(t){var e=this.$chips.eq(t);this.$chips.eq(t).remove(),this.$chips=this.$chips.filter(function(t){return 0<=h(t).index()}),this.chipsData.splice(t,1),this._setPlaceholder(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete.call(this,this.$el,e[0])}},{key:"selectChip",value:function(t){var e=this.$chips.eq(t);(this._selectedChip=e)[0].focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect.call(this,this.$el,e[0])}}],[{key:"init",value:function(t,e){return _get(l.__proto__||Object.getPrototypeOf(l),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Chips}},{key:"_handleChipsKeydown",value:function(t){l._keydown=!0;var e=h(t.target).closest(".chips"),i=t.target&&e.length;if(!h(t.target).is("input, textarea")&&i){var n=e[0].M_Chips;if(8===t.keyCode||46===t.keyCode){t.preventDefault();var s=n.chipsData.length;if(n._selectedChip){var o=n._selectedChip.index();n.deleteChip(o),n._selectedChip=null,s=Math.max(o-1,0)}n.chipsData.length&&n.selectChip(s)}else if(37===t.keyCode){if(n._selectedChip){var a=n._selectedChip.index()-1;if(a<0)return;n.selectChip(a)}}else if(39===t.keyCode&&n._selectedChip){var r=n._selectedChip.index()+1;r>=n.chipsData.length?n.$input[0].focus():n.selectChip(r)}}}},{key:"_handleChipsKeyup",value:function(t){l._keydown=!1}},{key:"_handleChipsBlur",value:function(t){l._keydown||(h(t.target).closest(".chips")[0].M_Chips._selectedChip=null)}},{key:"defaults",get:function(){return e}}]),l}();t._keydown=!1,M.Chips=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"chips","M_Chips"),h(document).ready(function(){h(document.body).on("click",".chip .close",function(){var t=h(this).closest(".chips");t.length&&t[0].M_Chips||h(this).closest(".chip").remove()})})}(cash),function(s){"use strict";var e={top:0,bottom:1/0,offset:0,onPositionChange:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Pushpin=i).options=s.extend({},n.defaults,e),i.originalOffset=i.el.offsetTop,n._pushpins.push(i),i._setupEventHandlers(),i._updatePosition(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.el.style.top=null,this._removePinClasses(),this._removeEventHandlers();var t=n._pushpins.indexOf(this);n._pushpins.splice(t,1)}},{key:"_setupEventHandlers",value:function(){document.addEventListener("scroll",n._updateElements)}},{key:"_removeEventHandlers",value:function(){document.removeEventListener("scroll",n._updateElements)}},{key:"_updatePosition",value:function(){var t=M.getDocumentScrollTop()+this.options.offset;this.options.top<=t&&this.options.bottom>=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=this.options.offset+"px",this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),tthis.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}},{key:"_removePinClasses",value:function(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Pushpin}},{key:"_updateElements",value:function(){for(var t in n._pushpins){n._pushpins[t]._updatePosition()}}},{key:"defaults",get:function(){return e}}]),n}();t._pushpins=[],M.Pushpin=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"pushpin","M_Pushpin")}(cash),function(r,s){"use strict";var e={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};r.fn.reverse=[].reverse;var t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_FloatingActionButton=i).options=r.extend({},n.defaults,e),i.isOpen=!1,i.$anchor=i.$el.children("a").first(),i.$menu=i.$el.children("ul").first(),i.$floatingBtns=i.$el.find("ul .btn-floating"),i.$floatingBtnsReverse=i.$el.find("ul .btn-floating").reverse(),i.offsetY=0,i.offsetX=0,i.$el.addClass("direction-"+i.options.direction),"top"===i.options.direction?i.offsetY=40:"right"===i.options.direction?i.offsetX=-40:"bottom"===i.options.direction?i.offsetY=-40:i.offsetX=40,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_FloatingActionButton=void 0}},{key:"_setupEventHandlers",value:function(){this._handleFABClickBound=this._handleFABClick.bind(this),this._handleOpenBound=this.open.bind(this),this._handleCloseBound=this.close.bind(this),this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this._handleOpenBound),this.el.addEventListener("mouseleave",this._handleCloseBound)):this.el.addEventListener("click",this._handleFABClickBound)}},{key:"_removeEventHandlers",value:function(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this._handleOpenBound),this.el.removeEventListener("mouseleave",this._handleCloseBound)):this.el.removeEventListener("click",this._handleFABClickBound)}},{key:"_handleFABClick",value:function(){this.isOpen?this.close():this.open()}},{key:"_handleDocumentClick",value:function(t){r(t.target).closest(this.$menu).length||this.close()}},{key:"open",value:function(){this.isOpen||(this.options.toolbarEnabled?this._animateInToolbar():this._animateInFAB(),this.isOpen=!0)}},{key:"close",value:function(){this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this._handleCloseBound,!0),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),this._animateOutToolbar()):this._animateOutFAB(),this.isOpen=!1)}},{key:"_animateInFAB",value:function(){var e=this;this.$el.addClass("active");var i=0;this.$floatingBtnsReverse.each(function(t){s({targets:t,opacity:1,scale:[.4,1],translateY:[e.offsetY,0],translateX:[e.offsetX,0],duration:275,delay:i,easing:"easeInOutQuad"}),i+=40})}},{key:"_animateOutFAB",value:function(){var e=this;this.$floatingBtnsReverse.each(function(t){s.remove(t),s({targets:t,opacity:0,scale:.4,translateY:e.offsetY,translateX:e.offsetX,duration:175,easing:"easeOutQuad",complete:function(){e.$el.removeClass("active")}})})}},{key:"_animateInToolbar",value:function(){var t,e=this,i=window.innerWidth,n=window.innerHeight,s=this.el.getBoundingClientRect(),o=r('
    '),a=this.$anchor.css("background-color");this.$anchor.append(o),this.offsetX=s.left-i/2+s.width/2,this.offsetY=n-s.bottom,t=i/o[0].clientWidth,this.btnBottom=s.bottom,this.btnLeft=s.left,this.btnWidth=s.width,this.$el.addClass("active"),this.$el.css({"text-align":"center",width:"100%",bottom:0,left:0,transform:"translateX("+this.offsetX+"px)",transition:"none"}),this.$anchor.css({transform:"translateY("+-this.offsetY+"px)",transition:"none"}),o.css({"background-color":a}),setTimeout(function(){e.$el.css({transform:"",transition:"transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s"}),e.$anchor.css({overflow:"visible",transform:"",transition:"transform .2s"}),setTimeout(function(){e.$el.css({overflow:"hidden","background-color":a}),o.css({transform:"scale("+t+")",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"}),e.$menu.children("li").children("a").css({opacity:1}),e._handleDocumentClickBound=e._handleDocumentClick.bind(e),window.addEventListener("scroll",e._handleCloseBound,!0),document.body.addEventListener("click",e._handleDocumentClickBound,!0)},100)},0)}},{key:"_animateOutToolbar",value:function(){var t=this,e=window.innerWidth,i=window.innerHeight,n=this.$el.find(".fab-backdrop"),s=this.$anchor.css("background-color");this.offsetX=this.btnLeft-e/2+this.btnWidth/2,this.offsetY=i-this.btnBottom,this.$el.removeClass("active"),this.$el.css({"background-color":"transparent",transition:"none"}),this.$anchor.css({transition:"none"}),n.css({transform:"scale(0)","background-color":s}),this.$menu.children("li").children("a").css({opacity:""}),setTimeout(function(){n.remove(),t.$el.css({"text-align":"",width:"",bottom:"",left:"",overflow:"","background-color":"",transform:"translate3d("+-t.offsetX+"px,0,0)"}),t.$anchor.css({overflow:"",transform:"translate3d(0,"+t.offsetY+"px,0)"}),setTimeout(function(){t.$el.css({transform:"translate3d(0,0,0)",transition:"transform .2s"}),t.$anchor.css({transform:"translate3d(0,0,0)",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"})},20)},200)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FloatingActionButton}},{key:"defaults",get:function(){return e}}]),n}();M.FloatingActionButton=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"floatingActionButton","M_FloatingActionButton")}(cash,M.anime),function(g){"use strict";var e={autoClose:!1,format:"mmm dd, yyyy",parse:null,defaultDate:null,setDefaultDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,container:null,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onOpen:null,onClose:null,onDraw:null},t=function(t){function B(t,e){_classCallCheck(this,B);var i=_possibleConstructorReturn(this,(B.__proto__||Object.getPrototypeOf(B)).call(this,B,t,e));(i.el.M_Datepicker=i).options=g.extend({},B.defaults,e),e&&e.hasOwnProperty("i18n")&&"object"==typeof e.i18n&&(i.options.i18n=g.extend({},B.defaults.i18n,e.i18n)),i.options.minDate&&i.options.minDate.setHours(0,0,0,0),i.options.maxDate&&i.options.maxDate.setHours(0,0,0,0),i.id=M.guid(),i._setupVariables(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupEventHandlers(),i.options.defaultDate||(i.options.defaultDate=new Date(Date.parse(i.el.value)));var n=i.options.defaultDate;return B._isDate(n)?i.options.setDefaultDate?(i.setDate(n,!0),i.setInputValue()):i.gotoDate(n):i.gotoDate(new Date),i.isOpen=!1,i}return _inherits(B,Component),_createClass(B,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),g(this.modalEl).remove(),this.destroySelects(),this.el.M_Datepicker=void 0}},{key:"destroySelects",value:function(){var t=this.calendarEl.querySelector(".orig-select-year");t&&M.FormSelect.getInstance(t).destroy();var e=this.calendarEl.querySelector(".orig-select-month");e&&M.FormSelect.getInstance(e).destroy()}},{key:"_insertHTMLIntoDOM",value:function(){this.options.showClearBtn&&(g(this.clearBtn).css({visibility:""}),this.clearBtn.innerHTML=this.options.i18n.clear),this.doneBtn.innerHTML=this.options.i18n.done,this.cancelBtn.innerHTML=this.options.i18n.cancel,this.options.container?this.$modalEl.appendTo(this.options.container):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modalEl.id="modal-"+this.id,this.modal=M.Modal.init(this.modalEl,{onCloseEnd:function(){t.isOpen=!1}})}},{key:"toString",value:function(t){var e=this;return t=t||this.options.format,B._isDate(this.date)?t.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g).map(function(t){return e.formats[t]?e.formats[t]():t}).join(""):""}},{key:"setDate",value:function(t,e){if(!t)return this.date=null,this._renderDateDisplay(),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),B._isDate(t)){var i=this.options.minDate,n=this.options.maxDate;B._isDate(i)&&tn.maxDate||n.disableWeekends&&B._isWeekend(y)||n.disableDayFn&&n.disableDayFn(y),isEmpty:C,isStartRange:x,isEndRange:L,isInRange:T,showDaysInNextAndPreviousMonths:n.showDaysInNextAndPreviousMonths};l.push(this.renderDay($)),7==++_&&(r.push(this.renderRow(l,n.isRTL,m)),_=0,m=!(l=[]))}return this.renderTable(n,r,i)}},{key:"renderDay",value:function(t){var e=[],i="false";if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'';e.push("is-outside-current-month"),e.push("is-selection-disabled")}return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&(e.push("is-selected"),i="true"),t.hasEvent&&e.push("has-event"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"}},{key:"renderRow",value:function(t,e,i){return''+(e?t.reverse():t).join("")+""}},{key:"renderTable",value:function(t,e,i){return'
    '+this.renderHead(t)+this.renderBody(e)+"
    "}},{key:"renderHead",value:function(t){var e=void 0,i=[];for(e=0;e<7;e++)i.push(''+this.renderDayName(t,e,!0)+"");return""+(t.isRTL?i.reverse():i).join("")+""}},{key:"renderBody",value:function(t){return""+t.join("")+""}},{key:"renderTitle",value:function(t,e,i,n,s,o){var a,r,l=void 0,h=void 0,d=void 0,u=this.options,c=i===u.minYear,p=i===u.maxYear,v='
    ',f=!0,m=!0;for(d=[],l=0;l<12;l++)d.push('");for(a='",g.isArray(u.yearRange)?(l=u.yearRange[0],h=u.yearRange[1]+1):(l=i-u.yearRange,h=1+i+u.yearRange),d=[];l=u.minYear&&d.push('");r='";v+='',v+='
    ',u.showMonthAfterYear?v+=r+a:v+=a+r,v+="
    ",c&&(0===n||u.minMonth>=n)&&(f=!1),p&&(11===n||u.maxMonth<=n)&&(m=!1);return(v+='')+"
    "}},{key:"draw",value:function(t){if(this.isOpen||t){var e,i=this.options,n=i.minYear,s=i.maxYear,o=i.minMonth,a=i.maxMonth,r="";this._y<=n&&(this._y=n,!isNaN(o)&&this._m=s&&(this._y=s,!isNaN(a)&&this._m>a&&(this._m=a)),e="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(var l=0;l<1;l++)this._renderDateDisplay(),r+=this.renderTitle(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year,e)+this.render(this.calendars[l].year,this.calendars[l].month,e);this.destroySelects(),this.calendarEl.innerHTML=r;var h=this.calendarEl.querySelector(".orig-select-year"),d=this.calendarEl.querySelector(".orig-select-month");M.FormSelect.init(h,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),M.FormSelect.init(d,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),h.addEventListener("change",this._handleYearChange.bind(this)),d.addEventListener("change",this._handleMonthChange.bind(this)),"function"==typeof this.options.onDraw&&this.options.onDraw(this)}}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleInputChangeBound=this._handleInputChange.bind(this),this._handleCalendarClickBound=this._handleCalendarClick.bind(this),this._finishSelectionBound=this._finishSelection.bind(this),this._handleMonthChange=this._handleMonthChange.bind(this),this._closeBound=this.close.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("change",this._handleInputChangeBound),this.calendarEl.addEventListener("click",this._handleCalendarClickBound),this.doneBtn.addEventListener("click",this._finishSelectionBound),this.cancelBtn.addEventListener("click",this._closeBound),this.options.showClearBtn&&(this._handleClearClickBound=this._handleClearClick.bind(this),this.clearBtn.addEventListener("click",this._handleClearClickBound))}},{key:"_setupVariables",value:function(){var e=this;this.$modalEl=g(B._template),this.modalEl=this.$modalEl[0],this.calendarEl=this.modalEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.modalEl.querySelector(".year-text"),this.dateTextEl=this.modalEl.querySelector(".date-text"),this.options.showClearBtn&&(this.clearBtn=this.modalEl.querySelector(".datepicker-clear")),this.doneBtn=this.modalEl.querySelector(".datepicker-done"),this.cancelBtn=this.modalEl.querySelector(".datepicker-cancel"),this.formats={d:function(){return e.date.getDate()},dd:function(){var t=e.date.getDate();return(t<10?"0":"")+t},ddd:function(){return e.options.i18n.weekdaysShort[e.date.getDay()]},dddd:function(){return e.options.i18n.weekdays[e.date.getDay()]},m:function(){return e.date.getMonth()+1},mm:function(){var t=e.date.getMonth()+1;return(t<10?"0":"")+t},mmm:function(){return e.options.i18n.monthsShort[e.date.getMonth()]},mmmm:function(){return e.options.i18n.months[e.date.getMonth()]},yy:function(){return(""+e.date.getFullYear()).slice(2)},yyyy:function(){return e.date.getFullYear()}}}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("change",this._handleInputChangeBound),this.calendarEl.removeEventListener("click",this._handleCalendarClickBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleCalendarClick",value:function(t){if(this.isOpen){var e=g(t.target);e.hasClass("is-disabled")||(!e.hasClass("datepicker-day-button")||e.hasClass("is-empty")||e.parent().hasClass("is-disabled")?e.closest(".month-prev").length?this.prevMonth():e.closest(".month-next").length&&this.nextMonth():(this.setDate(new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"))),this.options.autoClose&&this._finishSelection()))}}},{key:"_handleClearClick",value:function(){this.date=null,this.setInputValue(),this.close()}},{key:"_handleMonthChange",value:function(t){this.gotoMonth(t.target.value)}},{key:"_handleYearChange",value:function(t){this.gotoYear(t.target.value)}},{key:"gotoMonth",value:function(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}},{key:"gotoYear",value:function(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}},{key:"_handleInputChange",value:function(t){var e=void 0;t.firedBy!==this&&(e=this.options.parse?this.options.parse(this.el.value,this.options.format):new Date(Date.parse(this.el.value)),B._isDate(e)&&this.setDate(e))}},{key:"renderDayName",value:function(t,e,i){for(e+=t.firstDay;7<=e;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}},{key:"_finishSelection",value:function(){this.setInputValue(),this.close()}},{key:"open",value:function(){if(!this.isOpen)return this.isOpen=!0,"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.draw(),this.modal.open(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.modal.close(),this}}],[{key:"init",value:function(t,e){return _get(B.__proto__||Object.getPrototypeOf(B),"init",this).call(this,this,t,e)}},{key:"_isDate",value:function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}},{key:"_isWeekend",value:function(t){var e=t.getDay();return 0===e||6===e}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"_getDaysInMonth",value:function(t,e){return[31,B._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}},{key:"_isLeapYear",value:function(t){return t%4==0&&t%100!=0||t%400==0}},{key:"_compareDates",value:function(t,e){return t.getTime()===e.getTime()}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Datepicker}},{key:"defaults",get:function(){return e}}]),B}();t._template=['"].join(""),M.Datepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"datepicker","M_Datepicker")}(cash),function(h){"use strict";var e={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},autoClose:!1,twelveHour:!0,vibrate:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onSelect:null},t=function(t){function f(t,e){_classCallCheck(this,f);var i=_possibleConstructorReturn(this,(f.__proto__||Object.getPrototypeOf(f)).call(this,f,t,e));return(i.el.M_Timepicker=i).options=h.extend({},f.defaults,e),i.id=M.guid(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupVariables(),i._setupEventHandlers(),i._clockSetup(),i._pickerSetup(),i}return _inherits(f,Component),_createClass(f,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),h(this.modalEl).remove(),this.el.M_Timepicker=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleClockClickStartBound=this._handleClockClickStart.bind(this),this._handleDocumentClickMoveBound=this._handleDocumentClickMove.bind(this),this._handleDocumentClickEndBound=this._handleDocumentClickEnd.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.plate.addEventListener("mousedown",this._handleClockClickStartBound),this.plate.addEventListener("touchstart",this._handleClockClickStartBound),h(this.spanHours).on("click",this.showView.bind(this,"hours")),h(this.spanMinutes).on("click",this.showView.bind(this,"minutes"))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleClockClickStart",value:function(t){t.preventDefault();var e=this.plate.getBoundingClientRect(),i=e.left,n=e.top;this.x0=i+this.options.dialRadius,this.y0=n+this.options.dialRadius,this.moved=!1;var s=f._Pos(t);this.dx=s.x-this.x0,this.dy=s.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMoveBound),document.addEventListener("touchmove",this._handleDocumentClickMoveBound),document.addEventListener("mouseup",this._handleDocumentClickEndBound),document.addEventListener("touchend",this._handleDocumentClickEndBound)}},{key:"_handleDocumentClickMove",value:function(t){t.preventDefault();var e=f._Pos(t),i=e.x-this.x0,n=e.y-this.y0;this.moved=!0,this.setHand(i,n,!1,!0)}},{key:"_handleDocumentClickEnd",value:function(t){var e=this;t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEndBound),document.removeEventListener("touchend",this._handleDocumentClickEndBound);var i=f._Pos(t),n=i.x-this.x0,s=i.y-this.y0;this.moved&&n===this.dx&&s===this.dy&&this.setHand(n,s),"hours"===this.currentView?this.showView("minutes",this.options.duration/2):this.options.autoClose&&(h(this.minutesView).addClass("timepicker-dial-out"),setTimeout(function(){e.done()},this.options.duration/2)),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMoveBound),document.removeEventListener("touchmove",this._handleDocumentClickMoveBound)}},{key:"_insertHTMLIntoDOM",value:function(){this.$modalEl=h(f._template),this.modalEl=this.$modalEl[0],this.modalEl.id="modal-"+this.id;var t=document.querySelector(this.options.container);this.options.container&&t?this.$modalEl.appendTo(t):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modal=M.Modal.init(this.modalEl,{onOpenStart:this.options.onOpenStart,onOpenEnd:this.options.onOpenEnd,onCloseStart:this.options.onCloseStart,onCloseEnd:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t),t.isOpen=!1}})}},{key:"_setupVariables",value:function(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.modalEl.querySelector(".timepicker-canvas"),this.plate=this.modalEl.querySelector(".timepicker-plate"),this.hoursView=this.modalEl.querySelector(".timepicker-hours"),this.minutesView=this.modalEl.querySelector(".timepicker-minutes"),this.spanHours=this.modalEl.querySelector(".timepicker-span-hours"),this.spanMinutes=this.modalEl.querySelector(".timepicker-span-minutes"),this.spanAmPm=this.modalEl.querySelector(".timepicker-span-am-pm"),this.footer=this.modalEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}},{key:"_pickerSetup",value:function(){var t=h('").appendTo(this.footer).on("click",this.clear.bind(this));this.options.showClearBtn&&t.css({visibility:""});var e=h('
    ');h('").appendTo(e).on("click",this.close.bind(this)),h('").appendTo(e).on("click",this.done.bind(this)),e.appendTo(this.footer)}},{key:"_clockSetup",value:function(){this.options.twelveHour&&(this.$amBtn=h('
    AM
    '),this.$pmBtn=h('
    PM
    '),this.$amBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm),this.$pmBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}},{key:"_buildSVGClock",value:function(){var t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,n=f._createSVGEl("svg");n.setAttribute("class","timepicker-svg"),n.setAttribute("width",i),n.setAttribute("height",i);var s=f._createSVGEl("g");s.setAttribute("transform","translate("+t+","+t+")");var o=f._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx",0),o.setAttribute("cy",0),o.setAttribute("r",4);var a=f._createSVGEl("line");a.setAttribute("x1",0),a.setAttribute("y1",0);var r=f._createSVGEl("circle");r.setAttribute("class","timepicker-canvas-bg"),r.setAttribute("r",e),s.appendChild(a),s.appendChild(r),s.appendChild(o),n.appendChild(s),this._canvas.appendChild(n),this.hand=a,this.bg=r,this.bearing=o,this.g=s}},{key:"_buildHoursView",value:function(){var t=h('
    ');if(this.options.twelveHour)for(var e=1;e<13;e+=1){var i=t.clone(),n=e/6*Math.PI,s=this.options.outerRadius;i.css({left:this.options.dialRadius+Math.sin(n)*s-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*s-this.options.tickRadius+"px"}),i.html(0===e?"00":e),this.hoursView.appendChild(i[0])}else for(var o=0;o<24;o+=1){var a=t.clone(),r=o/6*Math.PI,l=0'),e=0;e<60;e+=5){var i=t.clone(),n=e/30*Math.PI;i.css({left:this.options.dialRadius+Math.sin(n)*this.options.outerRadius-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*this.options.outerRadius-this.options.tickRadius+"px"}),i.html(f._addLeadingZero(e)),this.minutesView.appendChild(i[0])}}},{key:"_handleAmPmClick",value:function(t){var e=h(t.target);this.amOrPm=e.hasClass("am-btn")?"AM":"PM",this._updateAmPmView()}},{key:"_updateAmPmView",value:function(){this.options.twelveHour&&(this.$amBtn.toggleClass("text-primary","AM"===this.amOrPm),this.$pmBtn.toggleClass("text-primary","PM"===this.amOrPm))}},{key:"_updateTimeFromInput",value:function(){var t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(0','",""].join(""),M.Timepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"timepicker","M_Timepicker")}(cash),function(s){"use strict";var e={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_CharacterCounter=i).options=s.extend({},n.defaults,e),i.isInvalid=!1,i.isValidLength=!1,i._setupCounter(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}},{key:"_setupEventHandlers",value:function(){this._handleUpdateCounterBound=this.updateCounter.bind(this),this.el.addEventListener("focus",this._handleUpdateCounterBound,!0),this.el.addEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("focus",this._handleUpdateCounterBound,!0),this.el.removeEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_setupCounter",value:function(){this.counterEl=document.createElement("span"),s(this.counterEl).addClass("character-counter").css({float:"right","font-size":"12px",height:1}),this.$el.parent().append(this.counterEl)}},{key:"_removeCounter",value:function(){s(this.counterEl).remove()}},{key:"updateCounter",value:function(){var t=+this.$el.attr("data-length"),e=this.el.value.length;this.isValidLength=e<=t;var i=e;t&&(i+="/"+t,this._validateInput()),s(this.counterEl).html(i)}},{key:"_validateInput",value:function(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.$el.removeClass("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.$el.removeClass("valid"),this.$el.addClass("invalid"))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_CharacterCounter}},{key:"defaults",get:function(){return e}}]),n}();M.CharacterCounter=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"characterCounter","M_CharacterCounter")}(cash),function(b){"use strict";var e={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null},t=function(t){function i(t,e){_classCallCheck(this,i);var n=_possibleConstructorReturn(this,(i.__proto__||Object.getPrototypeOf(i)).call(this,i,t,e));return(n.el.M_Carousel=n).options=b.extend({},i.defaults,e),n.hasMultipleSlides=1'),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('
  • ');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i'+t.getAttribute("label")+"")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'":e.innerHTML,a=d("
  • "),r=d("");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime); \ No newline at end of file diff --git a/browser/templates/base.html b/browser/templates/base.html new file mode 100644 index 000000000..7434f6521 --- /dev/null +++ b/browser/templates/base.html @@ -0,0 +1,75 @@ + + + + + + + + Caliban + + + + + + + + + + + + + +
    + +
    + + {% block content %} + {% endblock %} + +
    + +
    + + + + + + + + + + + + {% block extraJs %} + {% endblock %} + + + diff --git a/browser/templates/footer.html b/browser/templates/footer.html deleted file mode 100644 index 49ea70248..000000000 --- a/browser/templates/footer.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    \ No newline at end of file diff --git a/browser/templates/form.html b/browser/templates/form.html deleted file mode 100644 index 13c9110d0..000000000 --- a/browser/templates/form.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Caliban - - - - - - - - -
    - {% include "navigation.html" %} - -
    -

    - Click on a file that you would like use to see how Caliban works! -

    -

    The .trk files allow you to annotate RAW264 cells across time-lapse frames while preserving relationship information.

    -

    - The .npz files allow you to annotate different types of data. test.npz is an example of organoid annotation across z-stack frames. test2.npz is an example of untracked HeLa cytoplasm in a time-lapse movie (45 frames). test3.npz is another of untracked and - uncorrected HeLa cytoplasm, but is only 5 frames. -

    -
    -
    - -

    - -
    -
    - {% include "footer.html" %} - - \ No newline at end of file diff --git a/browser/templates/index.html b/browser/templates/index.html new file mode 100644 index 000000000..b7909b8c7 --- /dev/null +++ b/browser/templates/index.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block content %} +
    + +
    +
    + Click on a file that you would like use to see how Caliban works! +
    +

    + The .trk files allow you to annotate RAW264 cells across time-lapse frames while preserving relationship information. +

    +

    + The .npz files allow you to annotate different types of data. + test.npz is an example of organoid annotation across z-stack frames. + test2.npz is a 3D example of nuclei in tissue. + test3.npz is an example of untracked and uncorrected HeLa cytoplasm across time. +

    +
    + +
    + +
    + +
    +

    + +

    +
    + +
    +

    + +

    +
    + +
    + +
    + +
    +{% endblock %} + +{% block extraJs %} + + +{% endblock %} diff --git a/browser/templates/index_track.html b/browser/templates/index_track.html deleted file mode 100644 index aaec04d78..000000000 --- a/browser/templates/index_track.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - Caliban - - - - - - - - - - - {% include "navigation.html" %} - {% include "infopane.html" %} - -
    - - - - - -
    - -

    Tracking Tool

    -

    Filename will be given after submit button is pressed.

    - -

    - CLICK HERE TO SUBMIT -

    - -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    frame:
    highlight cells:
    highlighted cells:
    edit mode:
    brush size:
    editing label:
    eraser:
    label:
    frames:
    parent:
    daughters:
    frame div:
    capped:
    state:
    -
    - -
    -
    -
    - - - - - - {% include "footer.html" %} - - \ No newline at end of file diff --git a/browser/templates/index_zstack.html b/browser/templates/index_zstack.html deleted file mode 100644 index 9e25df8a9..000000000 --- a/browser/templates/index_zstack.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - Caliban - - - - - - - - - - - {% include "navigation.html" %} - {% include "infopane.html" %} - - -
    - - - - - -
    - -

    Z-Stack Tool

    -

    Filename will be given after submit button is pressed.

    - -

    - CLICK HERE TO SUBMIT -

    - -

    - - - - - - - - - - - - - - - - - - - - -
    frame:
    channel:
    feature:
    highlight cells:
    highlighted cells:
    edit mode:
    brush size:
    editing label:
    eraser:
    label:
    slices:
    state:
    -
    - -
    -
    -
    - - - - - {% include "footer.html" %} - - \ No newline at end of file diff --git a/browser/templates/infopane.html b/browser/templates/infopane.html deleted file mode 100644 index 2f3713661..000000000 --- a/browser/templates/infopane.html +++ /dev/null @@ -1,103 +0,0 @@ - -
    -

    Caliban is a tool designed for the correction of biological image annotations. Annotations can be corrected with whole-label modifications or pixel-level editing. Caliban displays images as well as useful information about the images to guide corrections.

    - -

    The right pane of Caliban displays an image of cells. By default, this shows the cell segmentation masks for that image, but can be toggled to display the raw image. Each cell value is displayed with a different color. In pixel-editing mode, the raw image is overlaid with its annotations. 

    - -

    The left pane of Caliban displays information. The frame number you are viewing will always be displayed in the top left corner. When you mouse over a cell in the right pane, information about that cell will be displayed in the bottom left corner. Cell selections and confirmations will also be displayed in the bottom left corner.

    - -

    The Keyboard Tools

    - -

    Universal

    - -

    Mouse over a cell mask to see cell label and lineage information.

    - -

    Scroll Wheel | In raw image or edit mode, scroll up/down to change the brightness of the image. 

    - -

    z | Switch between annotations and raw images; use this to check if your masks and labels match up to the actual cell image.

    - -

    e | Toggle between whole-label and pixel-editing modes. Note: this will not toggle between modes if anything is selected.

    - -

    esc | Cancel operation; do this if you click on a cell(s) and want to undo your click.

    - -

    To navigate through frames:

    - -

    a or left arrow key | Go back to previous frame

    - -

    d or right arrow key | Go forward to next frame

    - -

    Whole-label mode (default)

    - -

    space to confirm actions; this will apply some actions (create, replace, swap) to all frames.

    - -

    s to confirm the single-frame versions of create, replace, or swap.

    - -

    Click on a cell mask to select it.

    - -

    h | Highlight mode; if used, this will highlight the cells that are selected with a red mask.

    - -

    p | Predict relationships (npz files only); if used, this will reassign labels across frames. Does not detect cell divisions in timelapse movies.

    - -

    Single-Click Edit Operations:

    - -

    alt+click | flood label: hold down the alt key while clicking on a cell. This will flood the label you clicked on with a new label. This can be used to relabel a cell that has a duplicate label elsewhere in the same frame. 

    - -

    shift+click | trim pixels: hold down the shift key while clicking on a cell. This will trim away stray pixels from the part of the label you clicked on. This can be used to quickly clean up a cell annotation if there are disconnected pixels with that label that should not exist. 

    - -

    c | create: click on one cell mask (cell C). Hit “c” to create a new cell track starting from that frame. 

    - -

    x | delete: click on one cell mask (cell A). Hit "x" to delete the mask. This is useful when you have a couple pixels in a frame that represent a cell, but you want to remove that label.

    - -

    Two-Click Edit Operations:

    - -

    r | replace: click on one cell mask (cell A) then another (cell B). Hit “r” to replace all instances of cell B with cell A.

    - -

    p | parent (tracked files only): click on one cell mask (cell D) then another (cell E). Hit “p” to assign cell D as the parent of cell E. Cell D will now include cell E in its list of daughter cells, and cell E will list cell D as its parent.

    - -

    | swap: click on one cell mask (cell F) then another (cell G). Hit “s” to swap the cell values (does not swap parent/daughter information). This affects all frames of the movie.

    - -

    w | watershed: this function is to separate one cell mask (cell H) into two (cell H and cell I). Click on one pixel in a cell mask (cell H), then another pixel in the same cell mask. These points should correspond to the centers of the cells you are trying to separate. You can select these positions while you are looking at the raw channel. The first click should be on the cell that you want to remain cell H.

    - -

    Caliban currently doesn’t support an option to reverse an operation, so if a watershed operation produces a bad split, you may need to correct it in pixel-editing mode. 

    - -

    Pixel-editing mode

    - -

    Viewing:

    - -

    Scroll Wheel | Adjust brightness of raw image.

    - -

    h | Highlight mode; if used, pixels that match the current value of the brush will be displayed in red. (If using conversion brush, the label being overwritten does not change color, only the label that will replace it.)

    - -

    Click or click and drag to use the brush. Brush will draw the value that it is set to and will not overwrite other labels.

    - -

    x | Toggle eraser mode on or off. Eraser will only erase labels that the brush is set to.

    - -

    To change the size of the brush:

    - -

    down arrow key | Decrease the brush size

    - -

    up arrow key | Increase the brush size

    - -

    To change the value of the brush:

    - -

    - | Decrease the value of the brush

    - -

    = | Increase the value of the brush

    - -

    n | Set brush to an unused value

    - -

    p | Turn on color picker (click on a label to set brush to that value)

    - -

    r | Turn on conversion brush (allows you to replace one label with another without affecting background, follow prompts)

    - -

    Misc:

    - -

    t | Use thresholding tool (click and drag a bounding box around area to threshold)

    - - - - - - - -
    \ No newline at end of file diff --git a/browser/templates/navigation.html b/browser/templates/navigation.html deleted file mode 100644 index 81ee993c3..000000000 --- a/browser/templates/navigation.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/browser/templates/partials/infopane.html b/browser/templates/partials/infopane.html new file mode 100644 index 000000000..4f0ee8646 --- /dev/null +++ b/browser/templates/partials/infopane.html @@ -0,0 +1,126 @@ + +
      +
    • +
      + help_outline + Instructions (Click to expand/collapse) +
      +
      +

      + Caliban is a tool designed for the correction of biological image annotations. + Annotations can be corrected with whole-label modifications or pixel-level editing. + Caliban displays images as well as useful information about the images to guide corrections. +

      + +

      + The right pane of Caliban displays an image of cells. + By default, this shows the cell segmentation masks for that image, but can be toggled to display the raw image. + Each cell value is displayed with a different color. + In pixel-editing mode, the raw image is overlaid with its annotations. +

      + +

      + The left pane of Caliban displays information. + The frame number you are viewing will always be displayed in the top left corner. + When you mouse over a cell in the right pane, information about that cell will be displayed in the bottom left corner. + Cell selections and confirmations will also be displayed in the bottom left corner. +

      + +

      The Keyboard Tools

      + +
      Universal
      + +

      Mouse over a cell mask to see cell label and lineage information.

      + +

      Spacebar + click and drag to pan across the viewed image.

      + +

      Scroll Wheel | In raw image or edit mode, scroll up/down to change the contrast of the image. Hold down the shift key while scrolling to change the brightness. (These values can be reset by pressing 0.) 

      + +

      Alt+Scroll Wheel or - and = | Zoom in and out on the image.

      + +

      0 (zero) | Reset the brightness and contrast settings, if viewing the raw image or in pixel-editing mode. 

      + +

      z | Switch between annotations and raw images; use this to check if your masks and labels match up to the actual cell image.

      + +

      e | Toggle between whole-label and pixel-editing modes. Note: this will not toggle between modes if anything is selected.

      + +

      esc | Cancel operation; do this if you click on a cell(s) and want to undo your click.

      + +

      To navigate through frames:

      + +

      a or left arrow key | Go back to previous frame

      + +

      d or right arrow key | Go forward to next frame

      + +
      Whole-label mode (default)
      + +

      space to confirm actions; this will apply some actions (create, replace, swap) to all frames.

      + +

      s to confirm the single-frame versions of create, replace, or swap.

      + +

      Click on a cell mask to select it.

      + +

      h | Highlight mode; if used, this will highlight the cells that are selected with a red mask.

      + +

      p | Predict relationships (npz files only); if used, this will reassign labels across frames. Does not detect cell divisions in timelapse movies.

      + +

      Single-Click Edit Operations:

      + +

      alt+click | flood label: hold down the alt key while clicking on a cell. This will flood the label you clicked on with a new label. This can be used to relabel a cell that has a duplicate label elsewhere in the same frame. 

      + +

      shift+click | trim pixels: hold down the shift key while clicking on a cell. This will trim away stray pixels from the part of the label you clicked on. This can be used to quickly clean up a cell annotation if there are disconnected pixels with that label that should not exist. 

      + +

      c | create: click on one cell mask (cell C). Hit “c” to create a new cell track starting from that frame. 

      + +

      x | delete: click on one cell mask (cell A). Hit "x" to delete the mask. This is useful when you have a couple pixels in a frame that represent a cell, but you want to remove that label.

      + +

      Two-Click Edit Operations:

      + +

      r | replace: click on one cell mask (cell A) then another (cell B). Hit “r” to replace all instances of cell B with cell A.

      + +

      p | parent (tracked files only): click on one cell mask (cell D) then another (cell E). Hit “p” to assign cell D as the parent of cell E. Cell D will now include cell E in its list of daughter cells, and cell E will list cell D as its parent.

      + +

      | swap: click on one cell mask (cell F) then another (cell G). Hit “s” to swap the cell values (does not swap parent/daughter information). This affects all frames of the movie.

      + +

      w | watershed: this function is to separate one cell mask (cell H) into two (cell H and cell I). Click on one pixel in a cell mask (cell H), then another pixel in the same cell mask. These points should correspond to the centers of the cells you are trying to separate. You can select these positions while you are looking at the raw channel. The first click should be on the cell that you want to remain cell H.

      + +

      Caliban currently doesn’t support an option to reverse an operation, so if a watershed operation produces a bad split, you may need to correct it in pixel-editing mode. 

      + +
      Pixel-editing mode
      + +

      Viewing:

      + +

      Scroll Wheel | Adjust contrast of raw image. Hold down the shift key to change the brightness. (These values can be reset by pressing 0.)

      + +

      h | Highlight mode; if used, pixels that match the current value of the brush will be displayed in red. (If using conversion brush, the label being overwritten does not change color, only the label that will replace it.)

      + +

      Click or click and drag to use the brush. Brush will draw the value that it is set to and will not overwrite other labels.

      + +

      x | Toggle eraser mode on or off. Eraser will only erase labels that the brush is set to.

      + +

      To change the size of the brush:

      + +

      down arrow key | Decrease the brush size

      + +

      up arrow key | Increase the brush size

      + +

      To change the value of the brush:

      + +

      [ | Decrease the value of the brush

      + +

      ] | Increase the value of the brush

      + +

      n | Set brush to an unused value

      + +

      p | Turn on color picker (click on a label to set brush to that value)

      + +

      r | Turn on conversion brush (allows you to replace one label with another without affecting background, follow prompts)

      + +

      Misc:

      + +

      t | Use thresholding tool (click and drag a bounding box around area to threshold)

      + +
      +
    • +
    + diff --git a/browser/templates/partials/infopane_abridged.html b/browser/templates/partials/infopane_abridged.html new file mode 100644 index 000000000..1a4e873c4 --- /dev/null +++ b/browser/templates/partials/infopane_abridged.html @@ -0,0 +1,89 @@ + +
      +
    • +
      + help_outline + Instructions (Click to expand/collapse) +
      +
      +

      + Caliban is a tool designed for the correction of biological image annotations. + Annotations can be corrected with whole-label modifications or pixel-level editing. + Caliban displays images as well as useful information about the images to guide corrections. +

      + +

      + The right pane of Caliban displays a multichannel image of cells. + By default, this shows the cell segmentation masks for that image, but can be toggled to display the raw image. + Each label can be displayed with a different color or as outlines overlaid on top of the raw image. + In pixel-editing mode, the raw image is overlaid with its annotations. +

      + +

      + The left pane of Caliban displays information. + When you mouse over a cell in the right pane, information about that cell will be displayed in the bottom left corner. + Cell selections and confirmations will also be displayed in the bottom left corner. +

      + +
      The Keyboard Tools
      + +
      Universal
      + +

      Mouse over a cell mask to see cell label and lineage information.

      + +

      Spacebar + click and drag to pan across the viewed image.

      + +

      Scroll Wheel | In raw image or edit mode, scroll up/down to change the contrast of the image. Hold down the shift key while scrolling to change the brightness. (These values can be reset by pressing 0.) 

      + +

      Alt+Scroll Wheel or - and = | Zoom in and out on the image.

      + +

      0 (zero) | Reset the brightness and contrast settings, if viewing the raw image or in pixel-editing mode. 

      + +

      z | Switch between annotations and raw images; use this to check if your masks and labels match up to the actual cell image.

      + +

      e | Toggle between whole-label and pixel-editing modes. Note: this will not toggle between modes if anything is selected.

      + +

      esc | Cancel operation; do this if you click on a cell(s) and want to undo your click.

      + +
      Whole-label mode (default)
      + +

      L to toggle between solid-label and label outline displays.

      + +

      space to confirm actions.

      + +

      Click on a cell mask to select it.

      + +

      Single-Click Edit Operations:

      + +

      shift+click | trim pixels: hold down the shift key while clicking on a cell. This will trim away stray pixels from the part of the label you clicked on. This can be used to quickly clean up a cell annotation if there are disconnected pixels with that label that should not exist. 

      + +

      x | delete: click on one cell mask (cell A). Hit "x" to delete the mask. This will remove all pixels of that label mask.

      + +

      Two-Click Edit Operations:

      + +

      r | replace: click on one cell mask (cell A) then another (cell B). Hit “r” to replace all pixels of label B with label A.

      + +
      Pixel-editing mode
      + +

      Click or click and drag to use the brush. Brush will draw the value that it is set to and will not overwrite other labels.

      + +

      n | Set brush to an unused value

      + +

      p | Turn on color picker (click on a label to set brush to that value)

      + +

      x | Toggle eraser mode on or off. Eraser will only erase labels that the brush is set to.

      + +

      r | Turn on conversion brush (allows you to replace one label with another without affecting background, follow prompts)

      + +

      down arrow key | Decrease the brush size

      + +

      up arrow key | Increase the brush size

      + +

      - | Decrease the value of the brush

      + +

      = | Increase the value of the brush

      + +
      +
    • +
    + diff --git a/browser/templates/partials/loading-bar.html b/browser/templates/partials/loading-bar.html new file mode 100644 index 000000000..a0c3d2b48 --- /dev/null +++ b/browser/templates/partials/loading-bar.html @@ -0,0 +1,40 @@ + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + diff --git a/browser/templates/partials/track_table.html b/browser/templates/partials/track_table.html new file mode 100644 index 000000000..09f9484e9 --- /dev/null +++ b/browser/templates/partials/track_table.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    frame:
    highlight cells:
    highlighted cells:
    editing:
    brush size:
    editing label:
    eraser:
    label:
    frames:
    parent:
    daughters:
    frame div:
    capped:
    state:
    diff --git a/browser/templates/partials/zstack_table.html b/browser/templates/partials/zstack_table.html new file mode 100644 index 000000000..7bb33d3c1 --- /dev/null +++ b/browser/templates/partials/zstack_table.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + +
    frame:
    channel:
    feature:
    zoom:
    viewing (x):
    viewing (y):
    highlight cells:
    highlighted cells:
    editing:
    brush size:
    editing label:
    eraser:
    label:
    slices:
    state:
    diff --git a/browser/templates/tool.html b/browser/templates/tool.html new file mode 100644 index 000000000..ad3b9adc5 --- /dev/null +++ b/browser/templates/tool.html @@ -0,0 +1,85 @@ +{% extends "base.html" %} + +{%- block content -%} +{%- if settings['rgb'] -%} + {% include "partials/infopane_abridged.html" %} +{%- else -%} + {% include "partials/infopane.html" %} +{%- endif -%} + +
    +
    + +
    + +

    {{ title | title }}

    +

    Filename will be given after submit button is pressed.

    + +
    + +
    + +
    + +
    + {% include "partials/loading-bar.html" %} +
    + +
    + +
    +
    +

    Filename to COPY/PASTE:

    +
    {{ filename }}
    +
    +
    + +
    +
    + {%- if filetype == "zstack" -%} + {% include "partials/zstack_table.html" %} + {%- elif filetype == "track" -%} + {% include "partials/track_table.html" %} + {%- endif -%} +
    +
    + +
    + +
    + + + + +
    + +
    +
    +{%- endblock -%} + +{%- block extraJs -%} +{%- if filetype == "zstack" -%} + +{%- elif filetype == "track" -%} + +{%- endif -%} + + + + +{%- endblock -%} diff --git a/desktop/.DS_Store b/desktop/.DS_Store deleted file mode 100644 index b3c57750e..000000000 Binary files a/desktop/.DS_Store and /dev/null differ diff --git a/desktop/caliban.py b/desktop/caliban.py index 21036b400..6e2b1cde4 100755 --- a/desktop/caliban.py +++ b/desktop/caliban.py @@ -24,7 +24,7 @@ # limitations under the License. # ============================================================================== """Displaying and Curating annotations tracked over time in multiple frames.""" -from mode import Mode +from mode import Mode, Mode2D, Mode3D, ModeTrack import cv2 import json @@ -2002,10 +2002,6 @@ class TrackReview(CalibanWindow): possible_keys = {"label", "daughters", "frames", "parent", "frame_div", "capped"} - replace_prompt = ("\nReplace {} with {}?" - "\nSPACE = REPLACE IN ALL FRAMES" - "\nESC = CANCEL") - def __init__(self, filename, lineage, raw, tracked): self.filename = filename self.tracks = lineage @@ -2033,8 +2029,7 @@ def __init__(self, filename, lineage, raw, tracked): self.max_intensity = None self.x = 0 self.y = 0 - self.mode = Mode.none() - self.mode.update_prompt_additions = self.custom_prompt + self.mode = ModeTrack.none() self.adjustment = 0 self.highlight = False self.highlighted_cell_one = -1 @@ -2700,12 +2695,6 @@ def label_mode_question_keypress_helper(self, symbol, modifiers): self.action_erode_label() self.mode.clear() - def custom_prompt(self): - if self.mode.kind == "QUESTION": - if self.mode.action == "REPLACE": - self.mode.text = TrackReview.replace_prompt.format(self.mode.label_2, - self.mode.label_1) - def get_raw_current_frame(self): return self.raw[self.current_frame,:,:,0] @@ -3152,29 +3141,35 @@ def save(self): for track in empty_tracks: del self.tracks[track] - with tarfile.open(self.filename + ".trk", "w") as trks: - with tempfile.NamedTemporaryFile("w") as lineage_file: - json.dump(self.tracks, lineage_file, indent=1) - lineage_file.flush() - trks.add(lineage_file.name, "lineage.json") - - with tempfile.NamedTemporaryFile() as raw_file: - np.save(raw_file, self.raw) - raw_file.flush() - trks.add(raw_file.name, "raw.npy") - - with tempfile.NamedTemporaryFile() as tracked_file: - np.save(tracked_file, self.tracked) - tracked_file.flush() - trks.add(tracked_file.name, "tracked.npy") + try: + with tarfile.open(self.filename + ".trk", "w") as trks: + with tempfile.NamedTemporaryFile("w") as lineage_file: + json.dump(self.tracks, lineage_file, indent=1) + lineage_file.flush() + trks.add(lineage_file.name, "lineage.json") + + with tempfile.NamedTemporaryFile() as raw_file: + np.save(raw_file, self.raw) + raw_file.flush() + trks.add(raw_file.name, "raw.npy") + + with tempfile.NamedTemporaryFile() as tracked_file: + np.save(tracked_file, self.tracked) + tracked_file.flush() + trks.add(tracked_file.name, "tracked.npy") + + except FileNotFoundError: + print('Encountered FileNotFoundError when trying to save.\n' + 'Make sure you are still connected to the directory you loaded this file from.') + + # don't want a case where we raise an exception, + # that would defeat the purpose of trying to save the file + except Exception as e: + print('Unexpected error saving file.') + print(e.message) class ZStackReview(CalibanWindow): - save_prompt_text = ("\nSave current file?" - "\nSPACE = SAVE" - "\nT = SAVE AS .TRK FILE" - "\nESC = CANCEL") - def __init__(self, filename, raw, annotated, save_vars_mode): ''' Set object attributes to store raw and annotated images (arrays), @@ -3205,8 +3200,23 @@ def __init__(self, filename, raw, annotated, save_vars_mode): # file opens to the first channel self.channel = 0 + # should be robust to 3D, 4D, and some cases of 5D array sizes + self.dims = raw.ndim + if self.dims == 3: + print('Warning: Caliban is intended to open 4D arrays.' + ' Did you mean to open a 3D file?') + self.raw = np.expand_dims(self.raw, axis=0) + self.annotated = np.expand_dims(self.annotated, axis=0) + + elif self.dims == 5: + print('Warning: Caliban is intended to open 4D arrays.' + ' Did you mean to open a 5D file?') + self.raw = np.squeeze(self.raw, axis=0) + self.annotated = np.squeeze(self.annotated, axis=0) + # unpack the shape of the raw array - self.num_frames, self.height, self.width, self.channel_max = raw.shape + self.num_frames, self.height, self.width, self.channel_max = self.raw.shape + self.single_frame = self.num_frames == 1 # info dictionaries that will be populated with info about labels for # each feature of annotation array @@ -3222,10 +3232,17 @@ def __init__(self, filename, raw, annotated, save_vars_mode): try: first_key = list(self.cell_info[0])[0] display_info_types = self.cell_info[0][first_key] - self.display_info = [*sorted(set(display_info_types) - {'frames'})] + if self.single_frame: + self.display_info = list(sorted(set(display_info_types) - {'frames', 'slices'})) + else: + self.display_info = list(sorted(set(display_info_types) - {'frames'})) + # if there are no labels in the feature, hardcode the display info except: - self.display_info = ['label', 'slices'] + if self.single_frame: + self.display_info = ['label'] + else: + self.display_info = ['label', 'slices'] # open file to first frame of annotation stack self.current_frame = 0 @@ -3257,8 +3274,10 @@ def __init__(self, filename, raw, annotated, save_vars_mode): # self.mode keeps track of selected labels, pending actions, displaying # prompts and confirmation dialogue, using Mode class; start with Mode.none() # (nothing selected, no actions pending) - self.mode = Mode.none() - self.mode.update_prompt_additions = self.custom_prompt + if self.single_frame: + self.mode = Mode2D.none() + else: + self.mode = Mode3D.none() # start with highlighting option turned off and no labels highlighted self.highlight = False @@ -3279,11 +3298,6 @@ def __init__(self, filename, raw, annotated, save_vars_mode): # start pyglet event loop pyglet.app.run() - def custom_prompt(self): - if self.mode.kind == "QUESTION": - if self.mode.action == "SAVE": - self.mode.text = ZStackReview.save_prompt_text - def handle_threshold(self): ''' Helper function to do pre- and post-action bookkeeping for thresholding. @@ -3735,7 +3749,7 @@ def edit_mode_misc_keypress_helper(self, symbol, modifiers): if symbol == key.SPACE: self.save() self.mode.clear() - if symbol == key.T: + if symbol == key.T and self.num_frames > 1: self.save_as_trk() self.mode.clear() @@ -3903,7 +3917,7 @@ def label_mode_none_keypress_helper(self, symbol, modifiers): self.mode.update("QUESTION", action="SAVE") # PREDICT - if symbol == key.P: + if symbol == key.P and not self.single_frame: self.mode.update("QUESTION", action="PREDICT", **self.mode.info) # RELABEL @@ -4004,7 +4018,7 @@ def label_mode_question_keypress_helper(self, symbol, modifiers): ''' # RESPOND TO SAVE QUESTION if self.mode.action == "SAVE": - if symbol == key.T: + if symbol == key.T and not self.single_frame: self.save_as_trk() self.mode.clear() if symbol == key.SPACE: @@ -4013,18 +4027,19 @@ def label_mode_question_keypress_helper(self, symbol, modifiers): # RESPOND TO RELABEL QUESTION elif self.mode.action == "RELABEL": - if symbol == key.U: - self.action_relabel_unique() - self.mode.clear() - if symbol == key.P: - self.action_relabel_preserve() - self.mode.clear() - if symbol == key.S: - self.action_relabel_frame() - self.mode.clear() if symbol == key.SPACE: self.action_relabel_all_frames() self.mode.clear() + if not self.single_frame: + if symbol == key.U: + self.action_relabel_unique() + self.mode.clear() + if symbol == key.P: + self.action_relabel_preserve() + self.mode.clear() + if symbol == key.S: + self.action_relabel_frame() + self.mode.clear() # RESPOND TO PREDICT QUESTION elif self.mode.action == "PREDICT": @@ -4037,7 +4052,7 @@ def label_mode_question_keypress_helper(self, symbol, modifiers): # RESPOND TO CREATE QUESTION elif self.mode.action == "CREATE NEW": - if symbol == key.S: + if symbol == key.S and not self.single_frame: self.action_new_single_cell() self.mode.clear() if symbol == key.SPACE: @@ -4046,7 +4061,7 @@ def label_mode_question_keypress_helper(self, symbol, modifiers): # RESPOND TO REPLACE QUESTION elif self.mode.action == "REPLACE": - if symbol == key.S: + if symbol == key.S and not self.single_frame: self.action_replace_single() self.mode.clear() if symbol == key.SPACE: @@ -4055,7 +4070,7 @@ def label_mode_question_keypress_helper(self, symbol, modifiers): # RESPOND TO SWAP QUESTION elif self.mode.action == "SWAP": - if symbol == key.S: + if symbol == key.S and not self.single_frame: self.action_swap_single_frame() self.mode.clear() if symbol == key.SPACE: @@ -4875,16 +4890,39 @@ def save(self): self.raw and self.annotated are arrays to save in npz (self.raw should always remain unmodified, but self.annotated may be modified) ''' + # make sure has same dims as original + if self.dims == 3: + raw = np.squeeze(self.raw, axis=0) + ann = np.squeeze(self.annotated, axis=0) + elif self.dims == 4: + raw = self.raw + ann = self.annotated + elif self.dims == 5: + raw = np.expand_dims(self.raw, axis=0) + ann = np.expand_dims(self.annotated, axis=0) + # create filename to save as save_file = self.filename + "_save_version_{}.npz".format(self.save_version) - # if file was opened with variable names raw and annotated, save them that way - if self.save_vars_mode == 0: - np.savez(save_file, raw = self.raw, annotated = self.annotated) - # otherwise, save as X and y - else: - np.savez(save_file, X = self.raw, y = self.annotated) - # keep track of which version of the file this is - self.save_version += 1 + + try: + # if file was opened with variable names raw and annotated, save them that way + if self.save_vars_mode == 0: + np.savez(save_file, raw=raw, annotated=ann) + # otherwise, save as X and y + else: + np.savez(save_file, X=raw, y=ann) + # keep track of which version of the file this is + self.save_version += 1 + + except FileNotFoundError: + print('Encountered FileNotFoundError when trying to save.\n' + 'Make sure you are still connected to the directory you loaded this file from.') + + # don't want a case where we raise an exception, + # that would defeat the purpose of trying to save the file + except Exception as e: + print('Unexpected error saving file.') + print(e.message) def add_cell_info(self, feature, add_label, frame): ''' @@ -5100,21 +5138,32 @@ def save_as_trk(self): trk_ann = np.zeros((self.num_frames, self.height, self.width,1), dtype = self.annotated.dtype) trk_ann[:,:,:,0] = self.annotated[:,:,:,self.feature] - with tarfile.open(filename + ".trk", "w") as trks: - with tempfile.NamedTemporaryFile("w") as lineage_file: - json.dump(self.lineage, lineage_file, indent=1) - lineage_file.flush() - trks.add(lineage_file.name, "lineage.json") - - with tempfile.NamedTemporaryFile() as raw_file: - np.save(raw_file, trk_raw) - raw_file.flush() - trks.add(raw_file.name, "raw.npy") - - with tempfile.NamedTemporaryFile() as tracked_file: - np.save(tracked_file, trk_ann) - tracked_file.flush() - trks.add(tracked_file.name, "tracked.npy") + try: + with tarfile.open(filename + ".trk", "w") as trks: + with tempfile.NamedTemporaryFile("w") as lineage_file: + json.dump(self.lineage, lineage_file, indent=1) + lineage_file.flush() + trks.add(lineage_file.name, "lineage.json") + + with tempfile.NamedTemporaryFile() as raw_file: + np.save(raw_file, trk_raw) + raw_file.flush() + trks.add(raw_file.name, "raw.npy") + + with tempfile.NamedTemporaryFile() as tracked_file: + np.save(tracked_file, trk_ann) + tracked_file.flush() + trks.add(tracked_file.name, "tracked.npy") + + except FileNotFoundError: + print('Encountered FileNotFoundError when trying to save.\n' + 'Make sure you are still connected to the directory you loaded this file from.') + + # don't want a case where we raise an exception, + # that would defeat the purpose of trying to save the file + except Exception as e: + print('Unexpected error saving file.') + print(e.message) def on_or_off(toggle): if toggle: @@ -5357,4 +5406,3 @@ def review(filename): if __name__ == "__main__": review(sys.argv[1]) - diff --git a/desktop/mode.py b/desktop/mode.py index 58a2fa369..e3faaa140 100644 --- a/desktop/mode.py +++ b/desktop/mode.py @@ -29,7 +29,8 @@ def __init__(self, kind, **info): self.kind = kind self.info = info self.text = "" - self.update_prompt() + self.frame_text = "" + self.simple_answer = "SPACE = CONFIRM\nESC = CANCEL" def __getattr__(self, attrib): if attrib in self.info: @@ -50,120 +51,214 @@ def update(self, kind, **info): self.info = info self.update_prompt() + def fill_frame_text(self): + return "" + def update_prompt_additions(self): ''' Can be overridden by custom Caliban classes to implement specific prompts. ''' pass + def set_prompt_text(self): + if self.action == "FILL HOLE": + self.text = "\nSelect hole to fill in label {}.".format(self.label) + + elif self.action == "PICK COLOR": + self.text = "\nClick on a label to change the brush value to that value." + + elif self.action == "DRAW BOX": + self.text = "\nDraw a bounding box around the area you want to threshold." + + elif self.action == "CONVERSION BRUSH TARGET": + self.text = "\nClick on the label you want to draw OVER." + + elif self.action == "CONVERSION BRUSH VALUE": + self.text = ("\nClick on the label you want to draw WITH," + " or press N to set the brush to an unused label.") + + def set_question_single(self): + if self.action == "FLOOD CELL": + frame_insert = self.fill_frame_text() + self.text = ("\nFlood selected region of {} with new label{}?" + "\n{}").format(self.label, frame_insert, self.simple_answer) + + elif self.action == "TRIM PIXELS": + frame_insert = self.fill_frame_text() + self.text = ("\nTrim unconnected pixels away from selected region of label {}{}?" + "\n{}").format(self.label, frame_insert, self.simple_answer) + + elif self.action == "DELETE": + frame_insert = self.fill_frame_text() + self.text = ("\nDelete label {}{}?" + "\n{}").format(self.label, frame_insert, self.simple_answer) + + elif self.action == "EROSION DILATION": + frame_insert = self.fill_frame_text() + self.text = ("\nIncrementally change size of label {}{}?" + "\nT = SHRINK LABEL" + "\nY = EXPAND LABEL").format(self.label, frame_insert) + + def set_question_multiframe_options(self): + if self.action == "REPLACE": + self.text = ("\nReplace {} with {}?" + "\nSPACE = REPLACE IN ALL FRAMES" + "\nS = REPLACE IN FRAME {} ONLY" + "\nESC = CANCEL").format(self.label_2, self.label_1, self.frame_2) + + elif self.action == "SWAP": + if self.frame_1 == self.frame_2: + self.text = ("\nSwap {} & {}?" + "\nSPACE = SWAP IN ALL FRAMES" + "\nS = SWAP IN FRAME {} ONLY" + "\nESC = CANCEL").format(self.label_2, self.label_1, self.frame_2) + else: + self.text = ("\nSwap {} & {}?" + "\nSPACE = SWAP IN ALL FRAMES" + "\nESC = CANCEL").format(self.label_2, self.label_1) + + elif self.action == "CREATE NEW": + self.text = ("\nCreate new label from {0} in frame {1}?" + "\nSPACE = CREATE IN FRAME {1} AND ALL SUBSEQUENT FRAMES" + "\nS = CREATE IN FRAME {1} ONLY" + "\nESC = CANCEL").format(self.label, self.frame) + + elif self.action == "PREDICT": + self.text = ("\nPredict 3D relationships between labels?" + "\nS = PREDICT THIS FRAME FROM PREVIOUS FRAME" + "\nSPACE = PREDICT ALL FRAMES" + "\nESC = CANCEL") + + elif self.action == "RELABEL": + self.text = ("\nRelabel annotations?" + "\nSPACE = RELABEL IN ALL FRAMES" + "\nP = PRESERVE 3D INFO WHILE RELABELING" + "\nS = RELABEL THIS FRAME ONLY" + "\nU = UNIQUELY RELABEL EACH LABEL" + "\nESC = CANCEL") + def update_prompt(self): - text = "" - answer = "SPACE = CONFIRM\nESC = CANCEL" + self.text = "" if self.kind == "SELECTED": - text = "\nSELECTED {}".format(self.label) + self.text = "\nSELECTED {}".format(self.label) elif self.kind == "MULTIPLE": - text = "\nSELECTED {}, {}".format(self.label_1, self.label_2) + self.text = "\nSELECTED {}, {}".format(self.label_1, self.label_2) elif self.kind == "QUESTION": + + self.set_question_single() + + self.set_question_multiframe_options() + if self.action == "SAVE": - text = ("\nSave current file?" - "\nSPACE = SAVE" - "\nESC = CANCEL") + self.text = ("\nSave current file?" + "\nSPACE = SAVE" + "\nESC = CANCEL") - elif self.action == "REPLACE": - text = ("\nReplace {} with {}?" - "\nSPACE = REPLACE IN ALL FRAMES" - "\nS = REPLACE IN FRAME {} ONLY" - "\nESC = CANCEL").format(self.label_2, self.label_1, self.frame_2) + elif self.action == "WATERSHED": + self.text = ("\nPerform watershed to split {}?" + "\n{}").format(self.label_1, self.simple_answer) - elif self.action == "SWAP": - if self.frame_1 == self.frame_2: - text = ("\nSwap {} & {}?" - "\nSPACE = SWAP IN ALL FRAMES" - "\nS = SWAP IN FRAME {} ONLY" - "\nESC = CANCEL").format(self.label_2, self.label_1, self.frame_2) - else: - text = ("\nSwap {} & {}?" - "\nSPACE = SWAP IN ALL FRAMES" - "\nESC = CANCEL").format(self.label_2, self.label_1) + elif self.kind == "PROMPT": + self.set_prompt_text() - elif self.action == "PARENT": - text = "\nMake {} a daughter of {}?\n{}".format(self.label_2, self.label_1, answer) + elif self.kind == "DRAW": + self.text = ("\nUsing conversion brush to replace {} with {}." + "\nUse ESC to stop using the conversion brush.").format( + self.conversion_brush_target, self.conversion_brush_value) - elif self.action == "NEW TRACK": - text = ("\nCreate new track from {0} in frame {1}?" - "\nSPACE = CREATE IN FRAME {1} AND ALL SUBSEQUENT FRAMES" - "\nS = CREATE IN FRAME {1} ONLY" - "\nESC = CANCEL").format(self.label, self.frame) + self.update_prompt_additions() - elif self.action == "CREATE NEW": - text = ("\nCreate new label from {0} in frame {1}?" - "\nSPACE = CREATE IN FRAME {1} AND ALL SUBSEQUENT FRAMES" - "\nS = CREATE IN FRAME {1} ONLY" - "\nESC = CANCEL").format(self.label, self.frame) + @staticmethod + def none(): + return Mode(None) - elif self.action == "FLOOD CELL": - text = ("\nFlood selected region of {} with new label in frame {}?" - "\n{}").format(self.label, self.frame, answer) - elif self.action == "TRIM PIXELS": - text = ("\nTrim unconnected pixels away from selected region of label {} in frame {}?" - "\n{}").format(self.label, self.frame, answer) +class Mode2D(Mode): + ''' + don't need any information about frames + ''' - elif self.action == "EROSION DILATION": - text = ("\nIncrementally change size of label {} in frame {}?" - "\nT = SHRINK LABEL" - "\nY = EXPAND LABEL").format(self.label, self.frame) + def __init__(self, kind, **info): + super().__init__(kind, **info) + self.update_prompt() - elif self.action == "DELETE": - text = ("\nDelete label {} in frame {}?" - "\n{}").format(self.label, self.frame, answer) + def set_question_multiframe_options(self): + if self.kind == "QUESTION": - elif self.action == "WATERSHED": - text = ("\nPerform watershed to split {}?" - "\n{}").format(self.label_1, answer) + if self.action == "REPLACE": + self.text = "\nReplace {} with {}?\n{}".format( + self.label_2, self.label_1, self.simple_answer) + + elif self.action == "SWAP": + self.text = "\nSwap {} & {}?\n".format( + self.label_1, self.label_2, self.simple_answer) - elif self.action == "PREDICT": - text = ("\nPredict 3D relationships between labels?" - "\nS = PREDICT THIS FRAME FROM PREVIOUS FRAME" - "\nSPACE = PREDICT ALL FRAMES" - "\nESC = CANCEL") + elif self.action == "CREATE NEW": + self.text = "\nCreate new label from label {}?\n{}".format( + self.label, self.simple_answer) elif self.action == "RELABEL": - text = ("\nRelabel annotations?" - "\nSPACE = RELABEL IN ALL FRAMES" - "\nP = PRESERVE 3D INFO WHILE RELABELING" - "\nS = RELABEL THIS FRAME ONLY" - "\nU = UNIQUELY RELABEL EACH LABEL" - "\nESC = CANCEL") + self.text = "\nRelabel annotations?\n{}".format(self.simple_answer) - elif self.kind == "PROMPT": - if self.action == "FILL HOLE": - text = "\nSelect hole to fill in label {}.".format(self.label) + @staticmethod + def none(): + return Mode2D(None) - elif self.action == "PICK COLOR": - text = "\nClick on a label to change the brush value to that value." - elif self.action == "DRAW BOX": - text = "\nDraw a bounding box around the area you want to threshold." +class Mode3D(Mode): - elif self.action == "CONVERSION BRUSH TARGET": - text = "\nClick on the label you want to draw OVER." + def __init__(self, kind, **info): + super().__init__(kind, **info) + self.frame_text = " in frame {}" + self.update_prompt() - elif self.action == "CONVERSION BRUSH VALUE": - text = ("\nClick on the label you want to draw WITH," - " or press N to set the brush to an unused label.") + def fill_frame_text(self): + return self.frame_text.format(self.frame) - elif self.kind == "DRAW": - text = ("\nUsing conversion brush to replace {} with {}." - "\nUse ESC to stop using the conversion brush.").format(self.conversion_brush_target, - self.conversion_brush_value) + def update_prompt(self): + super().update_prompt() - self.text = text - self.update_prompt_additions() + if self.kind == "QUESTION": + if self.action == "SAVE": + self.text = ("\nSave current file?" + "\nSPACE = SAVE" + "\nT = SAVE AS .TRK FILE" + "\nESC = CANCEL") @staticmethod def none(): - return Mode(None) + return Mode3D(None) + + +class ModeTrack(Mode3D): + + def update_prompt(self): + super().update_prompt() + + if self.kind == "QUESTION": + if self.action == "SAVE": + self.text = ("\nSave current file?" + "\nSPACE = SAVE" + "\nESC = CANCEL") + + elif self.action == "PARENT": + self.text = "\nMake {} a daughter of {}?\n{}".format( + self.label_2, self.label_1, self.simple_answer) + + elif self.action == "NEW TRACK": + self.text = ("\nCreate new track from {0} in frame {1}?" + "\nSPACE = CREATE IN FRAME {1} AND ALL SUBSEQUENT FRAMES" + "\nS = CREATE IN FRAME {1} ONLY" + "\nESC = CANCEL").format(self.label, self.frame) + + elif self.action == "REPLACE": + self.text = ("\nReplace {} with {}?" + "\nSPACE = REPLACE IN ALL FRAMES" + "\nESC = CANCEL").format(self.label_2, self.label_1) + + @staticmethod + def none(): + return ModeTrack(None) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..49953b272 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,25 @@ +# Configuration of py.test +[pytest] +addopts=-v + --durations=20 + +# Ignore Deprecation Warnings +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + +# Do not run tests in the build folder +norecursedirs= + build + static + templates + +# PEP-8 The following are ignored: +# E501 line too long (82 > 79 characters) +# E731 do not assign a lambda expression, use a def +# W503 line break occurred before a binary operator + +pep8ignore=* E731 + +# Enable line length testing with maximum line length of 80 +pep8maxlinelength = 100