second commit

This commit is contained in:
2026-04-13 23:14:05 +02:00
parent 1c031ebf63
commit e08597fc72
125 changed files with 26221 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
from .celerymodule import appc as celery_app
__all__ = ('celery_app',)
__version__ = "0.1"
default_app_config = 'home.apps.HomeConfig'
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'
verbose_name = _('Scanner de tube à essais')
+29
View File
@@ -0,0 +1,29 @@
"""
ASGI config for home_automation project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'home.settings')
from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
import scanner.routing
application = ProtocolTypeRouter({
"http": django_asgi_app,
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(scanner.routing.websocket_urlpatterns)
),
),
})
+10
View File
@@ -0,0 +1,10 @@
#
# encoding: utf-8
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'home.settings')
appc = Celery('home')
appc.config_from_object('django.conf:settings', namespace='CELERY')
appc.autodiscover_tasks()
@@ -0,0 +1,11 @@
#
# encoding: utf-8
from django.conf import settings
def params(request):
return {
'APP_TITLE': settings.APP_TITLE,
'APP_SUB_TITLE': settings.APP_SUB_TITLE,
'self_url': request.build_absolute_uri(),
}
@@ -0,0 +1,89 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 08:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: home/apps.py:7
msgid "Automation"
msgstr ""
#: home/management/commands/init_data.py:63
msgid "Base configuration"
msgstr ""
#: home/management/commands/init_data.py:144
msgid "Non"
msgstr ""
#: home/management/commands/init_data.py:145
#: home/management/commands/init_data.py:148
msgid "Salon"
msgstr ""
#: home/management/commands/init_data.py:146
msgid "Cellier"
msgstr ""
#: home/management/commands/init_data.py:147
msgid "Cuisine"
msgstr ""
#: home/management/commands/init_data.py:149
msgid "Entrée"
msgstr ""
#: home/management/commands/init_data.py:150
msgid "Repas"
msgstr ""
#: home/management/commands/init_data.py:151
msgid "Chambre 1"
msgstr ""
#: home/management/commands/init_data.py:152
msgid "Chambre 2"
msgstr ""
#: home/management/commands/init_data.py:153
msgid "Chambre 3"
msgstr ""
#: home/templates/inc/404.html:9
msgid "Error: missing page."
msgstr ""
#: home/templates/inc/404.html:17
msgid "This page does not exist.<br>Please re-enter a new address."
msgstr ""
#: home/templates/inc/500.html:9
msgid "System error"
msgstr ""
#: home/templates/inc/500.html:17
msgid ""
"This page server can't run correctly.<br>Please contact the site "
"administrator."
msgstr ""
#: templates/admin/base_site.html:4
msgid "Django site admin"
msgstr ""
#: templates/admin/base_site.html:10
msgid "Django administration"
msgstr ""
@@ -0,0 +1,36 @@
'''
Created on 19 janv. 2026
@author: denis
'''
from django.core.management.base import BaseCommand
#from django.conf import settings
from scanner.tasks import export_images
from scanner.models import Configuration
class Command(BaseCommand):
help = "Exporter les images jpg dans un fichier ZIP"
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument("--uuid", type=str, help=f"Export video to jpg zip file with uuid")
def handle(self, *args, **options): # @UnusedVariable
try:
uuid = options.get('uuid')
conf = Configuration.objects.filter(active=True).first()
print(f"Export video to jpg zip file with uuid: {uuid}")
# Export images ZIP
job_zip = export_images(
uuid,
start_ts=None,
end_ts=None,
jpeg_quality=conf.video_jpeg_quality, # Qualité JPEG
)
print()
print(job_zip)
except Exception as e:
print("Export video to jpg zip file with uuid error", e)
@@ -0,0 +1,37 @@
'''
Created on 19 janv. 2026
@author: denis
'''
from django.core.management.base import BaseCommand
#from django.conf import settings
from scanner.tasks import export_videos
from scanner.models import Configuration
class Command(BaseCommand):
help = "Exporter les videos"
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument("--uuid", type=str, help=f"Export video")
def handle(self, *args, **options): # @UnusedVariable
try:
uuid = options.get('uuid')
conf = Configuration.objects.filter(active=True).first()
print(f"Export video uuid: {uuid}", conf)
job = export_videos(
uuid,
start_ts=None,
end_ts=None,
frame_rate=conf.video_frame_rate, # Frame rate de la vidéo exportée
opencv_fourcc_format=conf.opencv_fourcc_format, # Format de compression vidéo (ex: 'mp4v' pour MP4),
opencv_video_type=conf.opencv_video_type, # Type de vidéo exportée (ex: 'mp4', 'avi', 'mkv'),
)
print()
print(f"Export video: {job.id}")
except Exception as e:
print("Export video uuid error", e)
@@ -0,0 +1,90 @@
#
# encoding: utf-8
#import re
#from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.core.management import BaseCommand
from django_celery_beat.models import IntervalSchedule, CrontabSchedule, SolarSchedule
from django.contrib.auth import get_user_model
from scanner.models import Well
def create_Well():
try:
if Well.objects.count() == 0:
for name in [
'A1', 'A2', 'A3', 'A4', 'A5', 'A6',
'B1', 'B2', 'B3', 'B4', 'B5', 'B6',
'C1', 'C2', 'C3', 'C4', 'C5', 'C6',
'D1', 'D2', 'D3', 'D4', 'D5', 'D6',
]:
Well.objects.create(name=name, user_id=1)
print('Creating Well')
except Exception as e:
print(f'Creating well error {e}')
def create_user():
try:
User = get_user_model()
if User.objects.count() == 0:
for email, username, password, is_superuser in settings.ADMINS:
if is_superuser:
User.objects.create_superuser(
email=email,
username=username,
password=password,
is_active=True,
is_superuser=is_superuser,
)
else:
User.objects.create_user(
email=email,
username=username,
password=password,
is_active=True,
)
print(f'Creating {username} user with {password} password and email: {email} superuser: {is_superuser}')
except Exception as e:
print(f'Creating user error {e}')
def create_schedule():
try:
for n in [1, 2, 5, 10, 15, 20, 30, 40, 45, 50]:
IntervalSchedule.objects.create(every=n, period='minutes')
print('Creating IntervalSchedule minutes')
for n in [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]:
IntervalSchedule.objects.create(every=n, period='hours')
print('Creating IntervalSchedule hours')
for n in [1, 2, 3, 7, 15, 30, 60, 90, 180, 360]:
IntervalSchedule.objects.create(every=n, period='days')
print('Creating IntervalSchedule days')
CrontabSchedule.objects.create(minute='0', hour='4', day_of_week='*', timezone=settings.TIME_ZONE)
CrontabSchedule.objects.create(minute='0', hour='4', day_of_week='*/3', timezone=settings.TIME_ZONE)
CrontabSchedule.objects.create(minute='15', hour='4', day_of_week='*/3', timezone=settings.TIME_ZONE)
CrontabSchedule.objects.create(minute='30', hour='4', day_of_week='*/3', timezone=settings.TIME_ZONE)
CrontabSchedule.objects.create(minute='0', hour='18', day_of_week='*', timezone=settings.TIME_ZONE)
CrontabSchedule.objects.create(minute='15', hour='18', day_of_week='*/3', timezone=settings.TIME_ZONE)
CrontabSchedule.objects.create(minute='30', hour='18', day_of_week='*/3', timezone=settings.TIME_ZONE)
print('Creating CrontabSchedule')
SolarSchedule.objects.create(event='sunset', latitude=43.8, longitude=2.3)
SolarSchedule.objects.create(event='sunrise', latitude=43.8, longitude=2.3)
print('Creating SolarSchedule')
except Exception as e:
print(f'Creating schedule error {e}')
class Command(BaseCommand):
def handle(self, *args, **options):
try:
create_user()
create_schedule()
create_Well()
except Exception as e:
print(f'Creating monitor error {e}')
@@ -0,0 +1,20 @@
#
# encoding: utf-8
from django.core.management import BaseCommand
from django.urls import get_resolver
class Command(BaseCommand):
def handle(self, *args, **kwargs):
def show_urls(urllist, depth=0):
for entry in urllist:
print(entry.pattern)
if hasattr(entry, 'url_patterns'):
show_urls(entry.url_patterns, depth + 1)
show_urls(get_resolver().url_patterns)
@@ -0,0 +1,38 @@
'''
Created on 19 janv. 2026
@author: denis
'''
import asyncio
from django.core.management.base import BaseCommand
from scanner.export_tasks import remove_video_by_uuid
from scanner.models import MultiWell
async def remove_video(sid, multiwells):
for m in multiwells:
row_to_char = m.row_order.split(',')
for row in range(m.rows):
for col in range(m.cols):
uuid = f'{sid}-{m.position}-{row_to_char[row]}{col+1}'
filters = {"$and": [{"&session": { "$eq": sid} }]}
print(f"Delete video for {uuid} with filters {filters}")
await remove_video_by_uuid(uuid, when=filters)
class Command(BaseCommand):
help = "Démarre les tâches Celery."
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument("--session", type=int, default=0, help=f"Delete all videos from Session id")
def handle(self, *args, **options): # @UnusedVariable
try:
sid = options.get('session')
multiwels = [m for m in MultiWell.objects.filter(active=True).all() ]
asyncio.run(remove_video(sid, multiwels))
except Exception as e:
print("Delete all videos from Session error", e)
@@ -0,0 +1,23 @@
#
# encoding: utf-8
from uuid import uuid4
from django.core.management import BaseCommand
from django.core.management.utils import get_random_string;
class Command(BaseCommand):
def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument("--app", type=str, default='django', help=f"Default application django/reductstore")
parser.add_argument("--head", type=str, default='scanner', help=f"Default application django/reductstore")
def handle(self, *args, **options):
if options.get('app')=='django':
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#€%^&*(-_=+)"
sk = get_random_string(50, chars)
print(f'django-insecure-{sk}')
elif options.get('app')=='reductstore':
head = options.get('head')
print(f'{head}-{uuid4()}')
@@ -0,0 +1,26 @@
'''
Created on 19 janv. 2026
@author: denis
'''
from django.core.management.base import BaseCommand
from scanner import tasks as scanner_tasks
class Command(BaseCommand):
help = "Démarre les tâches Celery."
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
#parser.add_argument("--task", action="store", dest="task", default='start_all')
def handle(self, *args, **options): # @UnusedVariable
#task = options['task']
try:
scanner_tasks.scanner_start.delay() # @UndefinedVariable
scanner_tasks.replay_start.delay() # @UndefinedVariable
except Exception as e:
print(e)
@@ -0,0 +1,68 @@
'''
Created on 19 janv. 2026
@author: denis
'''
from django.core.management.base import BaseCommand
from modules.grbl import GRBLController, wait_for
import time
class Command(BaseCommand):
help = "Démarre les tâches Celery."
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
#parser.add_argument("--task", action="store", dest="task", default='start_all')
def handle(self, *args, **options): # @UnusedVariable
def check_limits(grbl):
grbl.send("$130=") # Limite max X
grbl.send("$131=") # Limite max Y
def check_speed_limits(grbl):
grbl.send("$110") # Vitesse max X
grbl.send("$111") # Vitesse max Y
grbl.send("$112") # Vitesse max Z
#task = options['task']
try:
grbl = GRBLController()
grbl.send("$$")
#print("clear_alarm")
#grbl.clear_alarm()
#print("home_machine")
#grbl.home_machine()
grbl.go_origin(feed=1000)
wait_for(4.0)
print("go_origin pos", grbl.get_mpos())
#grbl.go_origin(feed=1000)
#check_limits(grbl)
#print("home_machine")
#grbl.home_machine()
print("move_to 100, 100")
grbl.move_to(100, 100, feed=1500)
wait_for(4.0)
#print("halt")
#grbl.halt()
#wait_for(2.0)
#print("pos", grbl.get_mpos())
print("go_origin")
#grbl.go_origin(feed=2000)
grbl.move_to(0, 0, feed=3000)
wait_for(2.0)
print("pos", grbl.get_mpos())
except Exception as e:
print(e)
+15
View File
@@ -0,0 +1,15 @@
from django.conf import settings
from django.utils import translation
def SetDefaultLangMiddleware(get_response):
def middleware(request):
if not request.COOKIES.get('django_language'):
request.COOKIES['django_language'] = settings.LANGUAGE_CODE
translation.activate(settings.LANGUAGE_CODE)
elif request.COOKIES['django_language'] != settings.LANGUAGE_CODE:
request.COOKIES['django_language'] = settings.LANGUAGE_CODE
translation.activate(settings.LANGUAGE_CODE)
response = get_response(request)
return response
return middleware
+389
View File
@@ -0,0 +1,389 @@
"""
Django settings for monitoring project.
Generated by 'django-admin startproject' using Django 5.1.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from django.contrib.messages import constants as message_constants
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
PACKAGE_DIR = BASE_DIR.parent
APP_DATAS = PACKAGE_DIR / config('APP_DATAS')
print("Django application data:", APP_DATAS)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
DJANGO_APP = config('DJANGO_APP')
APP_NAME = DJANGO_APP
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
SECRET_KEY = config("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', cast=bool)
DOMAIN_SERVER = config("DOMAIN_SERVER")
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
ALLOWED_HOSTS += [DOMAIN_SERVER, '*']
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', cast=Csv())
CSRF_TRUSTED_ORIGINS += [f'http://{DOMAIN_SERVER}', f'https://{DOMAIN_SERVER}']
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin'
X_FRAME_OPTIONS = 'SAMEORIGIN'
# Application definition
INSTALLED_APPS = [
'daphne',
'channels',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_celery_beat',
'django_celery_results',
'celery',
'home',
'scanner',
]
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
#'contrib.auth.EmailBackend',
)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'home.middleware.SetDefaultLangMiddleware',
]
ROOT_URLCONF = 'home.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates', ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'home.context_processors.params',
],
},
},
]
WSGI_APPLICATION = 'home.wsgi.application'
ASGI_APPLICATION = 'home.asgi.application'
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Redis
#
REDIS_HOST=config("REDIS_HOST")
REDIS_PORT=int(config("REDIS_PORT"))
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [(REDIS_HOST, REDIS_PORT)],
"capacity": 5000, # default 100
"expiry": 10, # default 60
},
},
}
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASE_NAME = config('DATABASE_NAME')
DATABASE_USER = config('DATABASE_USER')
DATABASE_PASSWORD = config('DATABASE_PASSWORD')
SQLITE3=config('SQLITE3')
SQLITE3_PATH=config('SQLITE3_PATH')
MARIADB=config('MARIADB')
POSTGRES=config('POSTGRES')
DATABASES = {
SQLITE3: {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': f'{SQLITE3_PATH}db.sqlite3',
"OPTIONS": {
"timeout": 20,
},
},
MARIADB: {
'ENGINE': 'django.db.backends.mysql',
'OPTIONS' : {
"init_command": "SET foreign_key_checks = 0;SET sql_mode='STRICT_TRANS_TABLES';",
'charset': 'utf8mb4',
},
'NAME': DATABASE_NAME,
'USER': DATABASE_USER,
'PASSWORD': DATABASE_PASSWORD,
'HOST': config("DATABASE_MARIADB_HOST"),
'PORT': config("DATABASE_MARIADB_PORT", cast=int),
},
POSTGRES: {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': DATABASE_NAME,
'USER': DATABASE_USER,
'PASSWORD': DATABASE_PASSWORD,
'HOST': config("DATABASE_POSTGRES_HOST"),
'PORT': config("DATABASE_POSTGRES_PORT", cast=int),
},
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
#LANGUAGE_CODE = 'en-US'
LANGUAGE_CODE = config('LANGUAGE_CODE')
LANGUAGES = (
('fr', 'Français'),
('en', 'English'),
)
TIME_ZONE = config('TIME_ZONE')
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_CODE = config('LOCALE_CODE')
LOCALE_LC_ALL = config('LOCALE_LC_ALL')
LOCALE_PATHS = (
BASE_DIR / 'home' / 'locale',
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = APP_DATAS / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = APP_DATAS / 'media'
"""
STATICFILES_DIRS = [
BASE_DIR / 'xxx',
]
"""
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
## LOGGING
# CRITICAL=50, ERROR=40, WARN=30, INFO=20, DEBUG=10 and NOTSET=0
LOGGING_FILE = config('LOGGING_FILE')
IS_LOGGING = config('IS_LOGGING', cast=bool)
LOGGING = None if not IS_LOGGING else {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(asctime)s - %(message)s'
},
"celery": {
"format": "%(asctime)s [%(levelname)s] %(name)s%(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
'handlers': {
"celery_file": {
"class" : "logging.handlers.RotatingFileHandler",
"filename" : BASE_DIR / "logs" / "celery.log",
"maxBytes" : 10 * 1024 * 1024, # 10 Mo par fichier
"backupCount": 5, # 5 fichiers max en rotation
"formatter" : "celery",
"encoding" : "utf-8",
},
'file': {
'level': 'INFO',
'class':'logging.handlers.RotatingFileHandler',
'filename': BASE_DIR / "logs" / LOGGING_FILE,
'maxBytes': 1024*1024*15, # 15MB
'backupCount': 10,
'formatter': 'simple',
},
'null': {'class': 'logging.NullHandler', },
'console': {'class': 'logging.StreamHandler',},
},
'loggers': {
"django": {"handlers": ["null"], "level": "ERROR", "propagate": False,},
"uvicorn": {"handlers": ["null"], "level": "ERROR", "propagate": False,},
"daphne": {"handlers": ["null"], "level": "ERROR", "propagate": False,},
'axes': {"handlers": ['file',],"level": "INFO", "propagate": True, },
"celery": {"handlers": ["celery_file"], "level": "INFO", "propagate": False,},
"celery.task": {"handlers": ["celery_file"], "level": "INFO", "propagate": False, },
"celery.worker": {"handlers": ["celery_file"], "level": "INFO", "propagate": False, },
"scanner": {"handlers": ["file"], "level": "INFO", "propagate": False,},
},
}
# ACCOUNT
#
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = "/"
##
# application
SUPERADMIN_MAIL = config('SUPERADMIN_MAIL')
ADMINS = [
(SUPERADMIN_MAIL, config('SUPERADMIN'), config('SUPERADMIN_PASS'), True),
]
MANAGERS = ADMINS
# w3 css
MESSAGE_TAGS = {
message_constants.DEBUG: 'w3-purple',
message_constants.INFO: 'w3-blue',
message_constants.SUCCESS: 'w3-green',
message_constants.WARNING: 'w3-orange',
message_constants.ERROR: 'w3-red',
}
# Default css classes for widgets and labels
DEFAULT_CSS = {
'error': 'w3-panel w3-red', # displayed in the label
'errorlist': 'w3-padding-8 w3-red', # encloses the error list
'required': 'w3-text-indigo', # used in the label and label + input enclosing box. NB: w3-validate only works if the input precedes the label!
'label': 'w3-label',
'Textarea': 'w3-input w3-border',
'TextInput': 'w3-input w3-border',
'Select': 'w3-select w3-border',
}
# django mail
#
DEFAULT_TO_EMAIL = config("DEFAULT_TO_EMAIL")
DEFAULT_FROM_EMAIL = config("DEFAULT_FROM_EMAIL")
DEFAULT_EMAIL_CONTACT = config("DEFAULT_EMAIL_CONTACT")
EMAIL_BACKEND = config("EMAIL_BACKEND")
EMAIL_HOST = config("EMAIL_HOST")
EMAIL_PORT = config("EMAIL_PORT", cast=int)
EMAIL_HOST_USER = config("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool)
## CELERY tasks
#
#from celery.schedules import crontab
REDIS_HOST_PORT = f'{config("REDIS_HOST")}:{config("REDIS_PORT")}'
CELERY_RESULT_BACKEND = f"redis://{REDIS_HOST_PORT}/1"
CELERY_BROKER_URL = f"redis://{REDIS_HOST_PORT}/0"
CELERY_TASK_RESULT_EXPIRES=3600
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = False
CELERY_TIMEZONE=TIME_ZONE
DJANGO_CELERY_BEAT_TZ_AWARE = False
CELERY_ENABLE_UTC = True
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_EXTENDED = True
CELERY_RESULT_BACKEND = 'django-db'
DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH=191
## reductstore
#
REDUCTSTORE_TOKEN = config('REDUCTSTORE_TOKEN')
REDUCTSTORE_HOST = config('REDUCTSTORE_HOST')
REDUCTSTORE_PORT = config('REDUCTSTORE_PORT', cast=int)
REDUCTSTORE_URL = f'http://{REDUCTSTORE_HOST}:{REDUCTSTORE_PORT}'
REDUCTSTORE_PATH = config("REDUCTSTORE_PATH")
## servers app
#
USER = config('USER')
GROUP = config('GROUP')
SERVER_HOST_PORT = config('SERVER_HOST_PORT', cast=int)
SERVER_HOST_IP = config('SERVER_HOST_IP')
# ws
SCANNER_WEBSOCKET_ROUTE = 'ws/scanner'
REPLAY_WEBSOCKET_ROUTE = 'ws/replay'
# app
#
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240
APP_TITLE = config("APP_TITLE")
APP_SUB_TITLE = config("APP_SUB_TITLE")
DATETIME_FORMAT = '%d-%m-%Y-%m %H:%M:%S'
#===========================
# default configuration
#
#===========================
# rpicam 4056x3040 2028x1080 2028x1520
EXPORTS_LOCAL_PATH = config("EXPORTS_LOCAL_PATH")
EXPORT_REMOTE_PATH = config("EXPORT_REMOTE_PATH")
EXPORT_DESTINATIONS = ["local", "remote"]
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,220 @@
/* custom.css */
.w3-div-center { margin: 0 auto; }
.w3-hidden { display: none; }
.w3-no-border { border: 0px; }
.w3-no-resize { resize: none; }
.w3-resize-vertical { resize: vertical; }
.w3-resize-horizontal { resize: horiziontal; }
.w3-text-upper { text-transform: uppercase; }
.w3-transparent { background: transparent; }
.w3-nbsp { white-space: nowrap; }
a.w3-word-wrap { word-wrap: break-word; }
.w3-scroll-x { overflow-x: auto; overflow-y: hidden; }
.w3-scroll-y { overflow-x: hidden; overflow-y: auto; }
.w3-scroll { overflow: auto; }
.w3-show-off,
.w3-display-none { display: none; }
.w3-show-on,
.w3-display-block { display: block; }
.w3-inline { display:inline-block; vertical-align: middle;}
.w3-dropup { width:100%; overflow:hidden; display:inline-block; }
.w3-dropup-item { float:left; width:auto; border:none; display:block; outline:0; }
.w3-dropup .w3-dropup-content { display: none; position: absolute; bottom: 3.0em; z-index: 1; }
a.w3-no-deco:link,
a.w3-no-deco:visited { color: inherit; text-decoration: grey; }
a.w3-no-deco:hover { background-color: transparent; }
ul.w3-no-deco { list-style-type: none; }
hr.w3-bar-item.divider { margin: 0 .1em; padding: 0.025em; }
.zindex-0 { z-index: 0; }
.zindex-1 { z-index: 10; }
.zindex-2 { z-index: 100; }
.zindex-3 { z-index: 200; }
.zindex-4 { z-index: 400; }
.w3-no-padding,.w3-padding-0{ padding: 0; }
.w3-padding-01 { padding: .1em; }
.w3-padding-02 { padding: .2em; }
.w3-padding-1 { padding: .25em; }
.w3-padding-2 { padding: .5em; }
.w3-padding-3 { padding: .75em; }
.w3-padding-4 { padding: 1em; }
.w3-padding-5 { padding: 1.5em; }
.w3-padding-6 { padding: 2em; }
.w3-margin-auto { margin: 0 auto; }
.w3-no-margin, .w3-margin-0 { margin: 0; }
.w3-margin-01 { margin: .1em; }
.w3-margin-02 { margin: .2em; }
.w3-margin-1, .w3-margin-small { margin: .25em; }
.w3-margin-2, .w3-margin-medium { margin: .5em; }
.w3-margin-3 { margin: .75em; }
.w3-margin-4 { margin: 1em; }
.w3-margin-5 { margin: 1.5em; }
.w3-margin-6 { margin: 2em; }
.w3-margin-left-1 { margin-left: .25em; }
.w3-margin-left-2 { margin-left: .5em; }
.w3-margin-left-3 { margin-left: .75em; }
.w3-margin-left-4 { margin-left: 1em; }
.w3-margin-left-5 { margin-left: 1.5em; }
.w3-margin-left-6 { margin-left: 2em; }
.w3-margin-right-1 { margin-right: .25em; }
.w3-margin-right-2 { margin-right: .5em; }
.w3-margin-right-3 { margin-right: .75em; }
.w3-margin-right-4 { margin-right: 1em; }
.w3-margin-right-5 { margin-right: 1.5em; }
.w3-margin-right-6 { margin-right: 2em; }
.w3-margin-top-1 { margin-top: .25em; }
.w3-margin-top-2 { margin-top: .5em; }
.w3-margin-top-3 { margin-top: .75em; }
.w3-margin-top-4 { margin-top: 1em; }
.w3-margin-top-5 { margin-top: 1.5em; }
.w3-margin-top-6 { margin-top: 2em; }
.w3-margin-bottom-1 { margin-bottom: .25em; }
.w3-margin-bottom-2 { margin-bottom: .5em; }
.w3-margin-bottom-3 { margin-bottom: .75em; }
.w3-margin-bottom-4 { margin-bottom: 1em; }
.w3-margin-bottom-5 { margin-bottom: 1.5em; }
.w3-margin-bottom-6 { margin-bottom: 25em; }
.w1 { width: 1%; }
.w4 { width: 4%; }
.w5 { width: 5%; }
.w9 { width: 9%; }
.w10 { width: 10%; }
.w11 { width: 11%; }
.w15 { width: 15%; }
.w20 { width: 20%; }
.w25 { width: 25%; }
.w30 { width: 30%; }
.w35 { width: 35%; }
.w40 { width: 40%; }
.w45 { width: 45%; }
.w50 { width: 50%; }
.w55 { width: 55%; }
.w60 { width: 60%; }
.w65 { width: 55%; }
.w70 { width: 70%; }
.w75 { width: 75%; }
.w80 { width: 80%; }
.w85 { width: 85%; }
.w90 { width: 90%; }
.w95 { width: 95%; }
.w100 { width: 100%; }
.w3-dark-low,.w3-hover-dark-low:hover,.w3-dark-low,.w3-hover-dark-low:hover{color:#fff!important;background-color:#111!important}
.w3-dark-medium,.w3-hover-dark-medium:hover,.w3-dark-medium,.w3-hover-dark-medium:hover{color:#fff!important;background-color:#222!important}
.w3-dark-light,.w3-hover-dark-light:hover,.w3-dark-light,.w3-hover-dark-light:hover{color:#fff!important;background-color:#333!important}
.w3-dark-xlight,.w3-hover-dark-xlight:hover,.w3-dark-xlight,.w3-hover-dark-xlight:hover{color:#fff!important;background-color:#444!important}
.w3-dark-xxlight,.w3-hover-dark-xxlight:hover,.w3-dark-xxlight,.w3-hover-dark-xxlight:hover{color:#fff!important;background-color:#555!important}
.w3-dark-hight,.w3-hover-dark-hight:hover,.w3-dark-hight,.w3-hover-dark-hight:hover{color:#fff!important;background-color:#666!important}
.w3-theme-light {color:#000 !important; background-color:#f5f7f8 !important}
.w3-theme-dark {color:#fff !important; background-color:#303e45 !important}
.w3-theme-action {color:#fff !important; background-color:#303e45 !important}
.w3-theme {color:#fff !important; background-color:#607d8b !important}
.w3-text-theme {color:#607d8b !important}
.w3-border-theme {border-color:#607d8b !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#607d8b !important}
.w3-hover-text-theme:hover {color:#607d8b !important}
.w3-hover-border-theme:hover {border-color:#607d8b !important}
.w3-flex-container, .w3-flex-container-row {
display: flex;
flex-direction: row;
justify-content: start;
align-content: start;
flex-wrap: wrap;
}
.w3-flex-container-col {
display: flex;
flex-direction: row;
justify-content: start;
align-content: start;
flex-wrap: wrap;
}
.w3-flex-container-center {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-content: center;
align-items: top;
margin: auto;
}
.w3-flex-item { order: 0; flex: 0 1 auto; align-self: auto; margin: .25em;}
.w3-flex-wrap { display: flex; flex-wrap: wrap; }
.w3-flex-wrap.align { align-items: center; justify-content: center; }
.cursor-alias {cursor: alias;}
.cursor-all-scroll {cursor: all-scroll;}
.cursor-auto {cursor: auto;}
.cursor-cell {cursor: cell;}
.cursor-context-menu {cursor: context-menu;}
.cursor-col-resize {cursor: col-resize;}
.cursor-copy {cursor: copy;}
.cursor-crosshair {cursor: crosshair;}
.cursor-default {cursor: default;}
.cursor-e-resize {cursor: e-resize;}
.cursor-ew-resize {cursor: ew-resize;}
.cursor-grab {cursor: -webkit-grab; cursor: grab;}
.cursor-grabbing {cursor: -webkit-grabbing; cursor: grabbing;}
.cursor-help {cursor: help;}
.cursor-move {cursor: move;}
.cursor-n-resize {cursor: n-resize;}
.cursor-ne-resize {cursor: ne-resize;}
.cursor-nesw-resize {cursor: nesw-resize;}
.cursor-ns-resize {cursor: ns-resize;}
.cursor-nw-resize {cursor: nw-resize;}
.cursor-nwse-resize {cursor: nwse-resize;}
.cursor-no-drop {cursor: no-drop;}
.cursor-none {cursor: none;}
.cursor-not-allowed {cursor: not-allowed;}
.cursor-pointer {cursor: pointer;}
.cursor-progress {cursor: progress;}
.cursor-row-resize {cursor: row-resize;}
.cursor-s-resize {cursor: s-resize;}
.cursor-se-resize {cursor: se-resize;}
.cursor-sw-resize {cursor: sw-resize;}
.cursor-text {cursor: text;}
.cursor-w-resize {cursor: w-resize;}
.cursor-wait {cursor: wait;}
.cursor-zoom-in {cursor: zoom-in;}
.cursor-zoom-out {cursor: zoom-out;}
/* Extra small devices (phones, 600px and down) */
@media only screen and (max-width: 600px) {
div.main-layout {background: transparent; margin:0.5em auto; width:100%; font-size: 1em; min-width:300px;}
}
/* Small devices (portrait tablets and large phones, 600px and up) */
@media only screen and (min-width: 600px) {
div.main-layout {background: transparent; margin:1em auto; width:95%; font-size: 1.1em; min-width:300px;}
}
/* Medium devices (landscape tablets, 768px and up) */
@media only screen and (min-width: 768px) {
div.main-layout {background: transparent; width:85%;font-size: 1.3em;}
}
/* Large devices (laptops/desktops, 992px and up) */
@media only screen and (min-width: 992px) {
div.main-layout {background: transparent; width:75%;}
}
/* Extra large devices (large laptops and desktops, 1200px and up) */
@media only screen and (min-width: 1200px) {
div.main-layout {background: transparent; width:70%; }
}
+251
View File
@@ -0,0 +1,251 @@
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#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}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[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{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{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-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
.w3-danger{color:#fff!important;background-color:#dd0000!important}
.w3-note{color:#000!important;background-color:#fff599!important}
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
.w3-warning{color:#000!important;background-color:#ffb305!important}
.w3-success{color:#fff!important;background-color:#008a00!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -0,0 +1,42 @@
/*
** custom.js
** GNU GENERAL PUBLIC LICENSE: (c) DD miraceti.net return document.querySelectorAll(id);
*/
let s = function(sel) { return document.querySelector(sel); };
let ss = function(sel) { return document.querySelectorAll(sel); };
let sId = function(sel) { return document.getElementById(sel); };
let isJsonObject = function(value) { return value !== undefined && value !== null && typeof value === 'object'; }
let sidebarOpen = function(width, left, right) { right.style.marginLeft=width; left.style.width=width; left.style.display="block"; };
let sidebarClose = function(left, right) { right.style.marginLeft="0"; left.style.display="none"; }
let sidebarAction = function(width, left, right) { (left.style.display==="none") ? sidebarOpen(width, left, right): sidebarClose(left, right); };
let goFullscreen = function() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else { document.exitFullscreen(); } };
let uuidFromId = function(id) { return id.getAttribute('id').split('-')[1]; }
let strReplace = function(str, c) { return str.replace(/\s+g/, c); };
let rtrim = function(str, c) { return str.replace(/\s+$/, c); };
let ltrim = function(str, c) { return str.replace(/^\s+/, c); };
let lastString = function(str, c) { let n = str.split(c); return n[n.length - 1]; };
let toggleCheckboxesByName = function(name) {
const boxes = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
const shouldCheck = Array.from(boxes).some(b => !b.checked);
boxes.forEach(b => b.checked = shouldCheck);
};
let toggleDisplay = function(el) {
const current = window.getComputedStyle(el).display;
el.style.display = (current === 'none') ? 'block': 'none';
};
function toLocalISOString(date) {
const offset = date.getTimezoneOffset() * 60000; // Convertir le décalage en millisecondes
const localDate = new Date(date.getTime() - offset);
return localDate.toISOString().slice(0, -1); // Retire le 'Z' final
}
function timestampToLocalISOString(timestamp) {
const date = new Date(timestamp*1000);
return toLocalISOString(date);
}
+400
View File
@@ -0,0 +1,400 @@
/* W3.JS 1.04 April 2019 by w3schools.com */
"use strict";
var w3 = {};
w3.hide = function (sel) {
w3.hideElements(w3.getElements(sel));
};
w3.hideElements = function (elements) {
var i, l = elements.length;
for (i = 0; i < l; i++) {
w3.hideElement(elements[i]);
}
};
w3.hideElement = function (element) {
w3.styleElement(element, "display", "none");
};
w3.show = function (sel, a) {
var elements = w3.getElements(sel);
if (a) {w3.hideElements(elements);}
w3.showElements(elements);
};
w3.showElements = function (elements) {
var i, l = elements.length;
for (i = 0; i < l; i++) {
w3.showElement(elements[i]);
}
};
w3.showElement = function (element) {
w3.styleElement(element, "display", "block");
};
w3.addStyle = function (sel, prop, val) {
w3.styleElements(w3.getElements(sel), prop, val);
};
w3.styleElements = function (elements, prop, val) {
var i, l = elements.length;
for (i = 0; i < l; i++) {
w3.styleElement(elements[i], prop, val);
}
};
w3.styleElement = function (element, prop, val) {
element.style.setProperty(prop, val);
};
w3.toggleShow = function (sel) {
var i, x = w3.getElements(sel), l = x.length;
for (i = 0; i < l; i++) {
if (x[i].style.display == "none") {
w3.styleElement(x[i], "display", "block");
} else {
w3.styleElement(x[i], "display", "none");
}
}
};
w3.addClass = function (sel, name) {
w3.addClassElements(w3.getElements(sel), name);
};
w3.addClassElements = function (elements, name) {
var i, l = elements.length;
for (i = 0; i < l; i++) {
w3.addClassElement(elements[i], name);
}
};
w3.addClassElement = function (element, name) {
var i, arr1, arr2;
arr1 = element.className.split(" ");
arr2 = name.split(" ");
for (i = 0; i < arr2.length; i++) {
if (arr1.indexOf(arr2[i]) == -1) {element.className += " " + arr2[i];}
}
};
w3.removeClass = function (sel, name) {
w3.removeClassElements(w3.getElements(sel), name);
};
w3.removeClassElements = function (elements, name) {
var i, l = elements.length, arr1, arr2, j;
for (i = 0; i < l; i++) {
w3.removeClassElement(elements[i], name);
}
};
w3.removeClassElement = function (element, name) {
var i, arr1, arr2;
arr1 = element.className.split(" ");
arr2 = name.split(" ");
for (i = 0; i < arr2.length; i++) {
while (arr1.indexOf(arr2[i]) > -1) {
arr1.splice(arr1.indexOf(arr2[i]), 1);
}
}
element.className = arr1.join(" ");
};
w3.toggleClass = function (sel, c1, c2) {
w3.toggleClassElements(w3.getElements(sel), c1, c2);
};
w3.toggleClassElements = function (elements, c1, c2) {
var i, l = elements.length;
for (i = 0; i < l; i++) {
w3.toggleClassElement(elements[i], c1, c2);
}
};
w3.toggleClassElement = function (element, c1, c2) {
var t1, t2, t1Arr, t2Arr, j, arr, allPresent;
t1 = (c1 || "");
t2 = (c2 || "");
t1Arr = t1.split(" ");
t2Arr = t2.split(" ");
arr = element.className.split(" ");
if (t2Arr.length == 0) {
allPresent = true;
for (j = 0; j < t1Arr.length; j++) {
if (arr.indexOf(t1Arr[j]) == -1) {allPresent = false;}
}
if (allPresent) {
w3.removeClassElement(element, t1);
} else {
w3.addClassElement(element, t1);
}
} else {
allPresent = true;
for (j = 0; j < t1Arr.length; j++) {
if (arr.indexOf(t1Arr[j]) == -1) {allPresent = false;}
}
if (allPresent) {
w3.removeClassElement(element, t1);
w3.addClassElement(element, t2);
} else {
w3.removeClassElement(element, t2);
w3.addClassElement(element, t1);
}
}
};
w3.getElements = function (id) {
if (typeof id == "object") {
return [id];
} else {
return document.querySelectorAll(id);
}
};
w3.filterHTML = function(id, sel, filter) {
var a, b, c, i, ii, iii, hit;
a = w3.getElements(id);
for (i = 0; i < a.length; i++) {
b = a[i].querySelectorAll(sel);
for (ii = 0; ii < b.length; ii++) {
hit = 0;
if (b[ii].innerText.toUpperCase().indexOf(filter.toUpperCase()) > -1) {
hit = 1;
}
c = b[ii].getElementsByTagName("*");
for (iii = 0; iii < c.length; iii++) {
if (c[iii].innerText.toUpperCase().indexOf(filter.toUpperCase()) > -1) {
hit = 1;
}
}
if (hit == 1) {
b[ii].style.display = "";
} else {
b[ii].style.display = "none";
}
}
}
};
w3.sortHTML = function(id, sel, sortvalue) {
var a, b, i, ii, y, bytt, v1, v2, cc, j;
a = w3.getElements(id);
for (i = 0; i < a.length; i++) {
for (j = 0; j < 2; j++) {
cc = 0;
y = 1;
while (y == 1) {
y = 0;
b = a[i].querySelectorAll(sel);
for (ii = 0; ii < (b.length - 1); ii++) {
bytt = 0;
if (sortvalue) {
v1 = b[ii].querySelector(sortvalue).innerText;
v2 = b[ii + 1].querySelector(sortvalue).innerText;
} else {
v1 = b[ii].innerText;
v2 = b[ii + 1].innerText;
}
v1 = v1.toLowerCase();
v2 = v2.toLowerCase();
if ((j == 0 && (v1 > v2)) || (j == 1 && (v1 < v2))) {
bytt = 1;
break;
}
}
if (bytt == 1) {
b[ii].parentNode.insertBefore(b[ii + 1], b[ii]);
y = 1;
cc++;
}
}
if (cc > 0) {break;}
}
}
};
w3.slideshow = function (sel, ms, func) {
var i, ss, x = w3.getElements(sel), l = x.length;
ss = {};
ss.current = 1;
ss.x = x;
ss.ondisplaychange = func;
if (!isNaN(ms) || ms == 0) {
ss.milliseconds = ms;
} else {
ss.milliseconds = 1000;
}
ss.start = function() {
ss.display(ss.current)
if (ss.ondisplaychange) {ss.ondisplaychange();}
if (ss.milliseconds > 0) {
window.clearTimeout(ss.timeout);
ss.timeout = window.setTimeout(ss.next, ss.milliseconds);
}
};
ss.next = function() {
ss.current += 1;
if (ss.current > ss.x.length) {ss.current = 1;}
ss.start();
};
ss.previous = function() {
ss.current -= 1;
if (ss.current < 1) {ss.current = ss.x.length;}
ss.start();
};
ss.display = function (n) {
w3.styleElements(ss.x, "display", "none");
w3.styleElement(ss.x[n - 1], "display", "block");
}
ss.start();
return ss;
};
w3.includeHTML = function(cb) {
var z, i, elmnt, file, xhttp;
z = document.getElementsByTagName("*");
for (i = 0; i < z.length; i++) {
elmnt = z[i];
file = elmnt.getAttribute("w3-include-html");
if (file) {
xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {elmnt.innerHTML = this.responseText;}
if (this.status == 404) {elmnt.innerHTML = "Page not found.";}
elmnt.removeAttribute("w3-include-html");
w3.includeHTML(cb);
}
}
xhttp.open("GET", file, true);
xhttp.send();
return;
}
}
if (cb) cb();
};
w3.getHttpData = function (file, func) {
w3.http(file, function () {
if (this.readyState == 4 && this.status == 200) {
func(this.responseText);
}
});
};
w3.getHttpObject = function (file, func) {
w3.http(file, function () {
if (this.readyState == 4 && this.status == 200) {
func(JSON.parse(this.responseText));
}
});
};
w3.displayHttp = function (id, file) {
w3.http(file, function () {
if (this.readyState == 4 && this.status == 200) {
w3.displayObject(id, JSON.parse(this.responseText));
}
});
};
w3.http = function (target, readyfunc, xml, method) {
var httpObj;
if (!method) {method = "GET"; }
if (window.XMLHttpRequest) {
httpObj = new XMLHttpRequest();
} else if (window.ActiveXObject) {
httpObj = new ActiveXObject("Microsoft.XMLHTTP");
}
if (httpObj) {
if (readyfunc) {httpObj.onreadystatechange = readyfunc;}
httpObj.open(method, target, true);
httpObj.send(xml);
}
};
w3.getElementsByAttribute = function (x, att) {
var arr = [], arrCount = -1, i, l, y = x.getElementsByTagName("*"), z = att.toUpperCase();
l = y.length;
for (i = -1; i < l; i += 1) {
if (i == -1) {y[i] = x;}
if (y[i].getAttribute(z) !== null) {arrCount += 1; arr[arrCount] = y[i];}
}
return arr;
};
w3.dataObject = {},
w3.displayObject = function (id, data) {
var htmlObj, htmlTemplate, html, arr = [], a, l, rowClone, x, j, i, ii, cc, repeat, repeatObj, repeatX = "";
htmlObj = document.getElementById(id);
htmlTemplate = init_template(id, htmlObj);
html = htmlTemplate.cloneNode(true);
arr = w3.getElementsByAttribute(html, "w3-repeat");
l = arr.length;
for (j = (l - 1); j >= 0; j -= 1) {
cc = arr[j].getAttribute("w3-repeat").split(" ");
if (cc.length == 1) {
repeat = cc[0];
} else {
repeatX = cc[0];
repeat = cc[2];
}
arr[j].removeAttribute("w3-repeat");
repeatObj = data[repeat];
if (repeatObj && typeof repeatObj == "object" && repeatObj.length != "undefined") {
i = 0;
for (x in repeatObj) {
i += 1;
rowClone = arr[j];
rowClone = w3_replace_curly(rowClone, "element", repeatX, repeatObj[x]);
a = rowClone.attributes;
for (ii = 0; ii < a.length; ii += 1) {
a[ii].value = w3_replace_curly(a[ii], "attribute", repeatX, repeatObj[x]).value;
}
(i === repeatObj.length) ? arr[j].parentNode.replaceChild(rowClone, arr[j]) : arr[j].parentNode.insertBefore(rowClone, arr[j]);
}
} else {
console.log("w3-repeat must be an array. " + repeat + " is not an array.");
continue;
}
}
html = w3_replace_curly(html, "element");
htmlObj.parentNode.replaceChild(html, htmlObj);
function init_template(id, obj) {
var template;
template = obj.cloneNode(true);
if (w3.dataObject.hasOwnProperty(id)) {return w3.dataObject[id];}
w3.dataObject[id] = template;
return template;
}
function w3_replace_curly(elmnt, typ, repeatX, x) {
var value, rowClone, pos1, pos2, originalHTML, lookFor, lookForARR = [], i, cc, r;
rowClone = elmnt.cloneNode(true);
pos1 = 0;
while (pos1 > -1) {
originalHTML = (typ == "attribute") ? rowClone.value : rowClone.innerHTML;
pos1 = originalHTML.indexOf("{{", pos1);
if (pos1 === -1) {break;}
pos2 = originalHTML.indexOf("}}", pos1 + 1);
lookFor = originalHTML.substring(pos1 + 2, pos2);
lookForARR = lookFor.split("||");
value = undefined;
for (i = 0; i < lookForARR.length; i += 1) {
lookForARR[i] = lookForARR[i].replace(/^\s+|\s+$/gm, ''); //trim
if (x) {value = x[lookForARR[i]];}
if (value == undefined && data) {value = data[lookForARR[i]];}
if (value == undefined) {
cc = lookForARR[i].split(".");
if (cc[0] == repeatX) {value = x[cc[1]]; }
}
if (value == undefined) {
if (lookForARR[i] == repeatX) {value = x;}
}
if (value == undefined) {
if (lookForARR[i].substr(0, 1) == '"') {
value = lookForARR[i].replace(/"/g, "");
} else if (lookForARR[i].substr(0,1) == "'") {
value = lookForARR[i].replace(/'/g, "");
}
}
if (value != undefined) {break;}
}
if (value != undefined) {
r = "{{" + lookFor + "}}";
if (typ == "attribute") {
rowClone.value = rowClone.value.replace(r, value);
} else {
w3_replace_html(rowClone, r, value);
}
}
pos1 = pos1 + 1;
}
return rowClone;
}
function w3_replace_html(a, r, result) {
var b, l, i, a, x, j;
if (a.hasAttributes()) {
b = a.attributes;
l = b.length;
for (i = 0; i < l; i += 1) {
if (b[i].value.indexOf(r) > -1) {b[i].value = b[i].value.replace(r, result);}
}
}
x = a.getElementsByTagName("*");
l = x.length;
a.innerHTML = a.innerHTML.replace(r, result);
}
};
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 675 KiB

@@ -0,0 +1,803 @@
<?xml version="1.0" standalone="no"?>
<!--
Font Awesome Free 5.10.2 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<metadata>
Created by FontForge 20190801 at Thu Aug 22 14:41:09 2019
By Robert Madole
Copyright (c) Font Awesome
</metadata>
<defs>
<font id="FontAwesome5Free-Regular" horiz-adv-x="512" >
<font-face
font-family="Font Awesome 5 Free Regular"
font-weight="400"
font-stretch="normal"
units-per-em="512"
panose-1="2 0 5 3 0 0 0 0 0 0"
ascent="448"
descent="-64"
bbox="-0.0663408 -64.0662 640.01 448.1"
underline-thickness="25"
underline-position="-50"
unicode-range="U+0020-F5C8"
/>
<missing-glyph />
<glyph glyph-name="heart" unicode="&#xf004;"
d="M458.4 383.7c75.2998 -63.4004 64.0996 -166.601 10.5996 -221.3l-175.4 -178.7c-10 -10.2002 -23.2998 -15.7998 -37.5996 -15.7998c-14.2002 0 -27.5996 5.69922 -37.5996 15.8994l-175.4 178.7c-53.5996 54.7002 -64.5996 157.9 10.5996 221.2
c57.8008 48.7002 147.101 41.2998 202.4 -15c55.2998 56.2998 144.6 63.5996 202.4 15zM434.8 196.2c36.2002 36.8994 43.7998 107.7 -7.2998 150.8c-38.7002 32.5996 -98.7002 27.9004 -136.5 -10.5996l-35 -35.7002l-35 35.7002
c-37.5996 38.2998 -97.5996 43.1992 -136.5 10.5c-51.2002 -43.1006 -43.7998 -113.5 -7.2998 -150.7l175.399 -178.7c2.40039 -2.40039 4.40039 -2.40039 6.80078 0z" />
<glyph glyph-name="star" unicode="&#xf005;" horiz-adv-x="576"
d="M528.1 276.5c26.2002 -3.7998 36.7002 -36.0996 17.7002 -54.5996l-105.7 -103l25 -145.5c4.5 -26.3008 -23.1992 -45.9004 -46.3994 -33.7002l-130.7 68.7002l-130.7 -68.7002c-23.2002 -12.2998 -50.8994 7.39941 -46.3994 33.7002l25 145.5l-105.7 103
c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2998l65.2998 132.4c11.7998 23.8994 45.7002 23.5996 57.4004 0l65.2998 -132.4zM388.6 135.7l100.601 98l-139 20.2002l-62.2002 126l-62.2002 -126l-139 -20.2002l100.601 -98l-23.7002 -138.4l124.3 65.2998
l124.3 -65.2998z" />
<glyph glyph-name="user" unicode="&#xf007;" horiz-adv-x="448"
d="M313.6 144c74.2002 0 134.4 -60.2002 134.4 -134.4v-25.5996c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v25.5996c0 74.2002 60.2002 134.4 134.4 134.4c28.7998 0 42.5 -16 89.5996 -16s60.9004 16 89.5996 16zM400 -16v25.5996
c0 47.6006 -38.7998 86.4004 -86.4004 86.4004c-14.6992 0 -37.8994 -16 -89.5996 -16c-51.2998 0 -75 16 -89.5996 16c-47.6006 0 -86.4004 -38.7998 -86.4004 -86.4004v-25.5996h352zM224 160c-79.5 0 -144 64.5 -144 144s64.5 144 144 144s144 -64.5 144 -144
s-64.5 -144 -144 -144zM224 400c-52.9004 0 -96 -43.0996 -96 -96s43.0996 -96 96 -96s96 43.0996 96 96s-43.0996 96 -96 96z" />
<glyph glyph-name="clock" unicode="&#xf017;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM317.8 96.4004l-84.8994 61.6992
c-3.10059 2.30078 -4.90039 5.90039 -4.90039 9.7002v164.2c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12v-141.7l66.7998 -48.5996c5.40039 -3.90039 6.5 -11.4004 2.60059 -16.7998l-18.8008 -25.9004c-3.89941 -5.2998 -11.3994 -6.5 -16.7998 -2.59961z
" />
<glyph glyph-name="list-alt" unicode="&#xf022;"
d="M464 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h416zM458 16c3.31152 0 6 2.68848 6 6v340c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-340
c0 -3.31152 2.68848 -6 6 -6h404zM416 108v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 204v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12
v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 300v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM164 288c0 -19.8818 -16.1182 -36 -36 -36
s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 192c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 96c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36z" />
<glyph glyph-name="flag" unicode="&#xf024;"
d="M336.174 368c35.4668 0 73.0195 12.6914 108.922 28.1797c31.6406 13.6514 66.9043 -9.65723 66.9043 -44.1162v-239.919c0 -16.1953 -8.1543 -31.3057 -21.7129 -40.1631c-26.5762 -17.3643 -70.0693 -39.9814 -128.548 -39.9814c-68.6084 0 -112.781 32 -161.913 32
c-56.5674 0 -89.957 -11.2803 -127.826 -28.5566v-83.4434c0 -8.83691 -7.16309 -16 -16 -16h-16c-8.83691 0 -16 7.16309 -16 16v406.438c-14.3428 8.2998 -24 23.7979 -24 41.5615c0 27.5693 23.2422 49.71 51.2012 47.8965
c22.9658 -1.49023 41.8662 -19.4717 44.4805 -42.3379c0.177734 -1.52441 0.321289 -4.00781 0.321289 -5.54199c0 -4.30176 -1.10352 -11.1035 -2.46289 -15.1846c22.418 8.68555 49.4199 15.168 80.7207 15.168c68.6084 0 112.781 -32 161.913 -32zM464 112v240
c-31.5059 -14.6338 -84.5547 -32 -127.826 -32c-59.9111 0 -101.968 32 -161.913 32c-41.4365 0 -80.4766 -16.5879 -102.261 -32v-232c31.4473 14.5967 84.4648 24 127.826 24c59.9111 0 101.968 -32 161.913 -32c41.4365 0 80.4775 16.5879 102.261 32z" />
<glyph glyph-name="bookmark" unicode="&#xf02e;" horiz-adv-x="384"
d="M336 448c26.5098 0 48 -21.4902 48 -48v-464l-192 112l-192 -112v464c0 26.5098 21.4902 48 48 48h288zM336 19.5703v374.434c0 3.31348 -2.68555 5.99609 -6 5.99609h-276c-3.31152 0 -6 -2.68848 -6 -6v-374.43l144 84z" />
<glyph glyph-name="image" unicode="&#xf03e;"
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM458 48c3.31152 0 6 2.68848 6 6v276c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-276
c0 -3.31152 2.68848 -6 6 -6h404zM128 296c22.0908 0 40 -17.9092 40 -40s-17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40zM96 96v48l39.5137 39.5146c4.6875 4.68652 12.2852 4.68652 16.9717 0l39.5146 -39.5146l119.514 119.515
c4.6875 4.68652 12.2852 4.68652 16.9717 0l87.5146 -87.5146v-80h-320z" />
<glyph glyph-name="edit" unicode="&#xf044;" horiz-adv-x="576"
d="M402.3 103.1l32 32c5 5 13.7002 1.5 13.7002 -5.69922v-145.4c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h273.5c7.09961 0 10.7002 -8.59961 5.7002 -13.7002l-32 -32c-1.5 -1.5 -3.5 -2.2998 -5.7002 -2.2998h-241.5v-352h352
v113.5c0 2.09961 0.799805 4.09961 2.2998 5.59961zM558.9 304.9l-262.601 -262.601l-90.3994 -10c-26.2002 -2.89941 -48.5 19.2002 -45.6006 45.6006l10 90.3994l262.601 262.601c22.8994 22.8994 59.8994 22.8994 82.6992 0l43.2002 -43.2002
c22.9004 -22.9004 22.9004 -60 0.100586 -82.7998zM460.1 274l-58.0996 58.0996l-185.8 -185.899l-7.2998 -65.2998l65.2998 7.2998zM524.9 353.7l-43.2002 43.2002c-4.10059 4.09961 -10.7998 4.09961 -14.7998 0l-30.9004 -30.9004l58.0996 -58.0996l30.9004 30.8994
c4 4.2002 4 10.7998 -0.0996094 14.9004z" />
<glyph glyph-name="times-circle" unicode="&#xf057;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM357.8 254.2l-62.2002 -62.2002l62.2002 -62.2002
c4.7002 -4.7002 4.7002 -12.2998 0 -17l-22.5996 -22.5996c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-62.2002 62.2002l-62.2002 -62.2002c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-22.5996 22.5996c-4.7002 4.7002 -4.7002 12.2998 0 17l62.2002 62.2002l-62.2002 62.2002
c-4.7002 4.7002 -4.7002 12.2998 0 17l22.5996 22.5996c4.7002 4.7002 12.2998 4.7002 17 0l62.2002 -62.2002l62.2002 62.2002c4.7002 4.7002 12.2998 4.7002 17 0l22.5996 -22.5996c4.7002 -4.7002 4.7002 -12.2998 0 -17z" />
<glyph glyph-name="check-circle" unicode="&#xf058;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200z
M396.204 261.733c4.66699 -4.70508 4.63672 -12.3037 -0.0673828 -16.9717l-172.589 -171.204c-4.70508 -4.66797 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66797 4.70605 -4.63672 12.3047 0.0683594 16.9717l22.7188 22.5361
c4.70508 4.66699 12.3027 4.63574 16.9697 -0.0693359l59.792 -60.2773l141.353 140.216c4.70508 4.66797 12.3027 4.6377 16.9697 -0.0673828z" />
<glyph glyph-name="question-circle" unicode="&#xf059;"
d="M256 440c136.957 0 248 -111.083 248 -248c0 -136.997 -111.043 -248 -248 -248s-248 111.003 -248 248c0 136.917 111.043 248 248 248zM256 -8c110.569 0 200 89.4697 200 200c0 110.529 -89.5088 200 -200 200c-110.528 0 -200 -89.5049 -200 -200
c0 -110.569 89.4678 -200 200 -200zM363.244 247.2c0 -67.0518 -72.4209 -68.084 -72.4209 -92.8633v-6.33691c0 -6.62695 -5.37305 -12 -12 -12h-45.6475c-6.62695 0 -12 5.37305 -12 12v8.65918c0 35.7451 27.1006 50.0342 47.5791 61.5156
c17.5615 9.84473 28.3242 16.541 28.3242 29.5791c0 17.2461 -21.999 28.6934 -39.7842 28.6934c-23.1885 0 -33.8936 -10.9775 -48.9424 -29.9697c-4.05664 -5.11914 -11.46 -6.07031 -16.666 -2.12402l-27.8232 21.0986
c-5.10742 3.87207 -6.25098 11.0654 -2.64453 16.3633c23.627 34.6934 53.7217 54.1846 100.575 54.1846c49.0713 0 101.45 -38.3037 101.45 -88.7998zM298 80c0 -23.1592 -18.8408 -42 -42 -42s-42 18.8408 -42 42s18.8408 42 42 42s42 -18.8408 42 -42z" />
<glyph glyph-name="eye" unicode="&#xf06e;" horiz-adv-x="576"
d="M288 304c0.0927734 0 0.244141 0.000976562 0.336914 0.000976562c61.6641 0 111.71 -50.0469 111.71 -111.711c0 -61.6631 -50.0459 -111.71 -111.71 -111.71s-111.71 50.0469 -111.71 111.71c0 8.71289 1.95898 22.5781 4.37305 30.9502
c6.93066 -3.94141 19.0273 -7.18457 27 -7.24023c30.9121 0 56 25.0879 56 56c-0.0556641 7.97266 -3.29883 20.0693 -7.24023 27c8.42383 2.62207 22.4189 4.8623 31.2402 5zM572.52 206.6c1.9209 -3.79883 3.47949 -10.3379 3.47949 -14.5947
s-1.55859 -10.7959 -3.47949 -14.5947c-54.1992 -105.771 -161.59 -177.41 -284.52 -177.41s-230.29 71.5898 -284.52 177.4c-1.9209 3.79883 -3.47949 10.3379 -3.47949 14.5947s1.55859 10.7959 3.47949 14.5947c54.1992 105.771 161.59 177.41 284.52 177.41
s230.29 -71.5898 284.52 -177.4zM288 48c98.6602 0 189.1 55 237.93 144c-48.8398 89 -139.27 144 -237.93 144s-189.09 -55 -237.93 -144c48.8398 -89 139.279 -144 237.93 -144z" />
<glyph glyph-name="eye-slash" unicode="&#xf070;" horiz-adv-x="640"
d="M634 -23c3.31738 -2.65137 6.00977 -8.25098 6.00977 -12.498c0 -3.10449 -1.57715 -7.58984 -3.51953 -10.0117l-10 -12.4902c-2.65234 -3.31152 -8.24707 -6 -12.4902 -6c-3.09961 0 -7.58008 1.57227 -10 3.50977l-598 467.49
c-3.31738 2.65137 -6.00977 8.25098 -6.00977 12.498c0 3.10449 1.57715 7.58984 3.51953 10.0117l10 12.4902c2.65234 3.31152 8.24707 6 12.4902 6c3.09961 0 7.58008 -1.57227 10 -3.50977zM296.79 301.53c6.33496 1.35059 16.7324 2.45801 23.21 2.46973
c60.4805 0 109.36 -47.9102 111.58 -107.85zM343.21 82.46c-6.33496 -1.34375 -16.7334 -2.44629 -23.21 -2.45996c-60.4697 0 -109.35 47.9102 -111.58 107.84zM320 336c-19.8799 0 -39.2803 -2.7998 -58.2197 -7.09961l-46.4102 36.29
c32.9199 11.8096 67.9297 18.8096 104.63 18.8096c122.93 0 230.29 -71.5898 284.57 -177.4c1.91992 -3.79883 3.47949 -10.3379 3.47949 -14.5947s-1.55957 -10.7959 -3.47949 -14.5947c-11.7197 -22.7598 -35.4189 -56.4092 -52.9004 -75.1104l-37.7402 29.5
c14.333 15.0156 34.0449 41.9854 44 60.2002c-48.8398 89 -139.279 144 -237.93 144zM320 48c19.8896 0 39.2803 2.7998 58.2197 7.08984l46.4102 -36.2803c-32.9199 -11.7598 -67.9297 -18.8096 -104.63 -18.8096c-122.92 0 -230.28 71.5898 -284.51 177.4
c-1.9209 3.79883 -3.47949 10.3379 -3.47949 14.5947s1.55859 10.7959 3.47949 14.5947c11.7168 22.7568 35.4111 56.4014 52.8896 75.1006l37.7402 -29.5c-14.3467 -15.0107 -34.0811 -41.9756 -44.0498 -60.1904c48.8496 -89 139.279 -144 237.93 -144z" />
<glyph glyph-name="calendar-alt" unicode="&#xf073;" horiz-adv-x="448"
d="M148 160h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM256 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-40zM352 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM256 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM160 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM352 76c0 -6.59961 -5.40039 -12 -12 -12h-40
c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="comment" unicode="&#xf075;"
d="M256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5c-3.80078 8.7998 -2 19 4.59961 26
c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004
l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
<glyph glyph-name="folder" unicode="&#xf07b;"
d="M464 320c26.5098 0 48 -21.4902 48 -48v-224c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h146.74c8.49023 0 16.6299 -3.37012 22.6299 -9.37012l54.6299 -54.6299h192zM464 48v224h-198.62
c-8.49023 0 -16.6299 3.37012 -22.6299 9.37012l-54.6299 54.6299h-140.12v-288h416z" />
<glyph glyph-name="folder-open" unicode="&#xf07c;" horiz-adv-x="576"
d="M527.9 224c37.6992 0 60.6992 -41.5 40.6992 -73.4004l-79.8994 -128c-8.7998 -14.0996 -24.2002 -22.5996 -40.7002 -22.5996h-400c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h160l64 -64h160c26.5 0 48 -21.5 48 -48v-48h47.9004zM48 330v-233.4l62.9004 104.2
c8.69922 14.4004 24.2998 23.2002 41.0996 23.2002h280v42c0 3.2998 -2.7002 6 -6 6h-173.9l-64 64h-134.1c-3.2998 0 -6 -2.7002 -6 -6zM448 48l80 128h-378.8l-77.2002 -128h376z" />
<glyph glyph-name="chart-bar" unicode="&#xf080;"
d="M396.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v230.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-230.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM204.8 96
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v198.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-198.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM300.8 96
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v134.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-134.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM496 48c8.83984 0 16 -7.16016 16 -16v-16
c0 -8.83984 -7.16016 -16 -16 -16h-464c-17.6699 0 -32 14.3301 -32 32v336c0 8.83984 7.16016 16 16 16h16c8.83984 0 16 -7.16016 16 -16v-320h448zM108.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v70.4004c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004
c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-70.4004c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004z" />
<glyph glyph-name="comments" unicode="&#xf086;" horiz-adv-x="576"
d="M532 61.7998c15.2998 -30.7002 37.4004 -54.5 37.7998 -54.7998c6.2998 -6.7002 8 -16.5 4.40039 -25c-3.7002 -8.5 -12 -14 -21.2002 -14c-53.5996 0 -96.7002 20.2998 -125.2 38.7998c-19 -4.39941 -39 -6.7998 -59.7998 -6.7998
c-86.2002 0 -159.9 40.4004 -191.3 97.7998c-9.7002 1.2002 -19.2002 2.7998 -28.4004 4.90039c-28.5 -18.6006 -71.7002 -38.7998 -125.2 -38.7998c-9.19922 0 -17.5996 5.5 -21.1992 14c-3.7002 8.5 -1.90039 18.2998 4.39941 25
c0.400391 0.399414 22.4004 24.1992 37.7002 54.8994c-27.5 27.2002 -44 61.2002 -44 98.2002c0 88.4004 93.0996 160 208 160c86.2998 0 160.3 -40.5 191.8 -98.0996c99.7002 -11.8008 176.2 -77.9004 176.2 -157.9c0 -37.0996 -16.5 -71.0996 -44 -98.2002zM139.2 154.1
l19.7998 -4.5c16 -3.69922 32.5 -5.59961 49 -5.59961c86.7002 0 160 51.2998 160 112s-73.2998 112 -160 112s-160 -51.2998 -160 -112c0 -28.7002 16.2002 -50.5996 29.7002 -64l24.7998 -24.5l-15.5 -31.0996c-2.59961 -5.10059 -5.2998 -10.1006 -8 -14.8008
c14.5996 5.10059 29 12.3008 43.0996 21.4004zM498.3 96c13.5 13.4004 29.7002 35.2998 29.7002 64c0 49.2002 -48.2998 91.5 -112.7 106c0.299805 -3.2998 0.700195 -6.59961 0.700195 -10c0 -80.9004 -78 -147.5 -179.3 -158.3
c29.0996 -29.6006 77.2998 -49.7002 131.3 -49.7002c16.5 0 33 1.90039 49 5.59961l19.9004 4.60059l17.0996 -11.1006c14.0996 -9.09961 28.5 -16.2998 43.0996 -21.3994c-2.69922 4.7002 -5.39941 9.7002 -8 14.7998l-15.5 31.0996z" />
<glyph glyph-name="star-half" unicode="&#xf089;" horiz-adv-x="576"
d="M288 62.7002v-54.2998l-130.7 -68.6006c-23.3994 -12.2998 -50.8994 7.60059 -46.3994 33.7002l25 145.5l-105.7 103c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2002l65.2998 132.4c5.90039 11.8994 17.2998 17.7998 28.7002 17.7998v-68.0996l-62.2002 -126
l-139 -20.2002l100.601 -98l-23.7002 -138.4z" />
<glyph glyph-name="lemon" unicode="&#xf094;"
d="M484.112 420.111c28.1221 -28.123 35.9434 -68.0039 19.0215 -97.0547c-23.0576 -39.584 50.1436 -163.384 -82.3311 -295.86c-132.301 -132.298 -256.435 -59.3594 -295.857 -82.3291c-29.0459 -16.917 -68.9219 -9.11426 -97.0576 19.0205
c-28.1221 28.1221 -35.9434 68.0029 -19.0215 97.0547c23.0566 39.5859 -50.1436 163.386 82.3301 295.86c132.308 132.309 256.407 59.3496 295.862 82.332c29.0498 16.9219 68.9307 9.09863 97.0537 -19.0234zM461.707 347.217
c13.5166 23.2031 -27.7578 63.7314 -50.4883 50.4912c-66.6025 -38.7939 -165.646 45.5898 -286.081 -74.8457c-120.444 -120.445 -36.0449 -219.472 -74.8447 -286.08c-13.542 -23.2471 27.8145 -63.6953 50.4932 -50.4883
c66.6006 38.7949 165.636 -45.5996 286.076 74.8428c120.444 120.445 36.0449 219.472 74.8447 286.08zM291.846 338.481c1.37012 -10.96 -6.40332 -20.957 -17.3643 -22.3271c-54.8467 -6.85547 -135.779 -87.7871 -142.636 -142.636
c-1.37305 -10.9883 -11.3984 -18.7334 -22.3262 -17.3643c-10.9609 1.37012 -18.7344 11.3652 -17.3643 22.3262c9.16211 73.2852 104.167 168.215 177.364 177.364c10.9531 1.36816 20.9561 -6.40234 22.3262 -17.3633z" />
<glyph glyph-name="credit-card" unicode="&#xf09d;" horiz-adv-x="576"
d="M527.9 416c26.5996 0 48.0996 -21.5 48.0996 -48v-352c0 -26.5 -21.5 -48 -48.0996 -48h-479.801c-26.5996 0 -48.0996 21.5 -48.0996 48v352c0 26.5 21.5 48 48.0996 48h479.801zM54.0996 368c-3.2998 0 -6 -2.7002 -6 -6v-42h479.801v42c0 3.2998 -2.7002 6 -6 6
h-467.801zM521.9 16c3.2998 0 6 2.7002 6 6v170h-479.801v-170c0 -3.2998 2.7002 -6 6 -6h467.801zM192 116v-40c0 -6.59961 -5.40039 -12 -12 -12h-72c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h72c6.59961 0 12 -5.40039 12 -12zM384 116v-40
c0 -6.59961 -5.40039 -12 -12 -12h-136c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h136c6.59961 0 12 -5.40039 12 -12z" />
<glyph glyph-name="hdd" unicode="&#xf0a0;" horiz-adv-x="576"
d="M567.403 212.358c5.59668 -8.04688 8.59668 -17.6113 8.59668 -27.4121v-136.946c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v136.946c0 8.30957 3.85156 20.5898 8.59668 27.4121l105.08 151.053
c7.90625 11.3652 25.5596 20.5889 39.4033 20.5889h0.000976562h269.838h0.000976562c13.8438 0 31.4971 -9.22363 39.4033 -20.5889zM153.081 336l-77.9131 -112h425.664l-77.9131 112h-269.838zM528 48v128h-480v-128h480zM496 112c0 -17.6729 -14.3271 -32 -32 -32
s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32zM400 112c0 -17.6729 -14.3271 -32 -32 -32s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32z" />
<glyph glyph-name="hand-point-right" unicode="&#xf0a4;"
d="M428.8 310.4c45.0996 0 83.2002 -38.1016 83.2002 -83.2002c0 -45.6162 -37.7646 -83.2002 -83.2002 -83.2002h-35.6475c-1.41602 -6.36719 -4.96875 -16.252 -7.92969 -22.0645c2.50586 -22.0059 -3.50293 -44.9775 -15.9844 -62.791
c-1.14062 -52.4863 -37.3984 -91.1445 -99.9404 -91.1445h-21.2988c-60.0635 0 -98.5117 40 -127.2 40h-2.67871c-5.74707 -4.95215 -13.5361 -8 -22.1201 -8h-64c-17.6729 0 -32 12.8936 -32 28.7998v230.4c0 15.9062 14.3271 28.7998 32 28.7998h64.001
c8.58398 0 16.373 -3.04785 22.1201 -8h2.67871c6.96387 0 14.8623 6.19336 30.1816 23.6689l0.128906 0.148438l0.130859 0.145508c8.85645 9.93652 18.1162 20.8398 25.8506 33.2529c18.7051 30.2471 30.3936 78.7842 75.707 78.7842c56.9277 0 92 -35.2861 92 -83.2002
v-0.0839844c0 -6.21777 -0.974609 -16.2148 -2.17578 -22.3154h86.1768zM428.8 192c18.9756 0 35.2002 16.2246 35.2002 35.2002c0 18.7002 -16.7754 35.2002 -35.2002 35.2002h-158.399c0 17.3242 26.3994 35.1992 26.3994 70.3994c0 26.4004 -20.625 35.2002 -44 35.2002
c-8.79395 0 -20.4443 -32.7119 -34.9258 -56.0996c-9.07422 -14.5752 -19.5244 -27.2256 -30.7988 -39.875c-16.1094 -18.374 -33.8359 -36.6328 -59.0752 -39.5967v-176.753c42.79 -3.7627 74.5088 -39.6758 120 -39.6758h21.2988
c40.5244 0 57.124 22.1973 50.6006 61.3252c14.6113 8.00098 24.1514 33.9785 12.9248 53.625c19.3652 18.2246 17.7871 46.3809 4.9502 61.0498h91.0254zM88 64c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
<glyph glyph-name="hand-point-left" unicode="&#xf0a5;"
d="M0 227.2c0 45.0986 38.1006 83.2002 83.2002 83.2002h86.1758c-1.3623 6.91016 -2.17578 14.374 -2.17578 22.3994c0 47.9141 35.0723 83.2002 92 83.2002c45.3135 0 57.002 -48.5371 75.7061 -78.7852c7.73438 -12.4121 16.9951 -23.3154 25.8506 -33.2529
l0.130859 -0.145508l0.128906 -0.148438c15.3213 -17.4746 23.2197 -23.668 30.1836 -23.668h2.67871c5.74707 4.95215 13.5361 8 22.1201 8h64c17.6729 0 32 -12.8936 32 -28.7998v-230.4c0 -15.9062 -14.3271 -28.7998 -32 -28.7998h-64
c-8.58398 0 -16.373 3.04785 -22.1201 8h-2.67871c-28.6885 0 -67.1367 -40 -127.2 -40h-21.2988c-62.542 0 -98.8008 38.6582 -99.9404 91.1445c-12.4814 17.8135 -18.4922 40.7852 -15.9844 62.791c-2.96094 5.8125 -6.51367 15.6973 -7.92969 22.0645h-35.6465
c-45.4355 0 -83.2002 37.584 -83.2002 83.2002zM48 227.2c0 -18.9756 16.2246 -35.2002 35.2002 -35.2002h91.0244c-12.8369 -14.6689 -14.415 -42.8252 4.9502 -61.0498c-11.2256 -19.6465 -1.68652 -45.624 12.9248 -53.625
c-6.52246 -39.1279 10.0771 -61.3252 50.6016 -61.3252h21.2988c45.4912 0 77.21 35.9131 120 39.6768v176.752c-25.2393 2.96289 -42.9658 21.2227 -59.0752 39.5967c-11.2744 12.6494 -21.7246 25.2998 -30.7988 39.875
c-14.4814 23.3877 -26.1318 56.0996 -34.9258 56.0996c-23.375 0 -44 -8.7998 -44 -35.2002c0 -35.2002 26.3994 -53.0752 26.3994 -70.3994h-158.399c-18.4248 0 -35.2002 -16.5 -35.2002 -35.2002zM448 88c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24
s24 10.7451 24 24s-10.7451 24 -24 24z" />
<glyph glyph-name="hand-point-up" unicode="&#xf0a6;" horiz-adv-x="448"
d="M105.6 364.8c0 45.0996 38.1016 83.2002 83.2002 83.2002c45.6162 0 83.2002 -37.7646 83.2002 -83.2002v-35.6465c6.36719 -1.41602 16.252 -4.96875 22.0645 -7.92969c22.0059 2.50684 44.9775 -3.50293 62.791 -15.9844
c52.4863 -1.14062 91.1445 -37.3984 91.1445 -99.9404v-21.2988c0 -60.0635 -40 -98.5117 -40 -127.2v-2.67871c4.95215 -5.74707 8 -13.5361 8 -22.1201v-64c0 -17.6729 -12.8936 -32 -28.7998 -32h-230.4c-15.9062 0 -28.7998 14.3271 -28.7998 32v64
c0 8.58398 3.04785 16.373 8 22.1201v2.67871c0 6.96387 -6.19336 14.8623 -23.6689 30.1816l-0.148438 0.128906l-0.145508 0.130859c-9.93652 8.85645 -20.8398 18.1162 -33.2529 25.8506c-30.2471 18.7051 -78.7842 30.3936 -78.7842 75.707
c0 56.9277 35.2861 92 83.2002 92h0.0839844c6.21777 0 16.2148 -0.974609 22.3154 -2.17578v86.1768zM224 364.8c0 18.9756 -16.2246 35.2002 -35.2002 35.2002c-18.7002 0 -35.2002 -16.7754 -35.2002 -35.2002v-158.399c-17.3242 0 -35.1992 26.3994 -70.3994 26.3994
c-26.4004 0 -35.2002 -20.625 -35.2002 -44c0 -8.79395 32.7119 -20.4443 56.0996 -34.9258c14.5752 -9.07422 27.2256 -19.5244 39.875 -30.7988c18.374 -16.1094 36.6328 -33.8359 39.5967 -59.0752h176.753c3.7627 42.79 39.6758 74.5088 39.6758 120v21.2988
c0 40.5244 -22.1973 57.124 -61.3252 50.6006c-8.00098 14.6113 -33.9785 24.1514 -53.625 12.9248c-18.2246 19.3652 -46.3809 17.7871 -61.0498 4.9502v91.0254zM352 24c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24z" />
<glyph glyph-name="hand-point-down" unicode="&#xf0a7;" horiz-adv-x="448"
d="M188.8 -64c-45.0986 0 -83.2002 38.1006 -83.2002 83.2002v86.1758c-6.91016 -1.3623 -14.374 -2.17578 -22.3994 -2.17578c-47.9141 0 -83.2002 35.0723 -83.2002 92c0 45.3135 48.5371 57.002 78.7852 75.707c12.4121 7.73438 23.3154 16.9951 33.2529 25.8506
l0.145508 0.130859l0.148438 0.128906c17.4746 15.3213 23.668 23.2197 23.668 30.1836v2.67871c-4.95215 5.74707 -8 13.5361 -8 22.1201v64c0 17.6729 12.8936 32 28.7998 32h230.4c15.9062 0 28.7998 -14.3271 28.7998 -32v-64.001
c0 -8.58398 -3.04785 -16.373 -8 -22.1201v-2.67871c0 -28.6885 40 -67.1367 40 -127.2v-21.2988c0 -62.542 -38.6582 -98.8008 -91.1445 -99.9404c-17.8135 -12.4814 -40.7852 -18.4922 -62.791 -15.9844c-5.8125 -2.96094 -15.6973 -6.51367 -22.0645 -7.92969v-35.6465
c0 -45.4355 -37.584 -83.2002 -83.2002 -83.2002zM188.8 -16c18.9756 0 35.2002 16.2246 35.2002 35.2002v91.0244c14.6689 -12.8369 42.8252 -14.415 61.0498 4.9502c19.6465 -11.2256 45.624 -1.68652 53.625 12.9248c39.1279 -6.52246 61.3252 10.0771 61.3252 50.6016
v21.2988c0 45.4912 -35.9131 77.21 -39.6768 120h-176.752c-2.96289 -25.2393 -21.2227 -42.9658 -39.5967 -59.0752c-12.6494 -11.2744 -25.2998 -21.7246 -39.875 -30.7988c-23.3877 -14.4814 -56.0996 -26.1318 -56.0996 -34.9258c0 -23.375 8.7998 -44 35.2002 -44
c35.2002 0 53.0752 26.3994 70.3994 26.3994v-158.399c0 -18.4248 16.5 -35.2002 35.2002 -35.2002zM328 384c0 -13.2549 10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24z" />
<glyph glyph-name="copy" unicode="&#xf0c5;" horiz-adv-x="448"
d="M433.941 382.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-80v-48c0 -26.5098 -21.4902 -48 -48 -48h-224c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h80v48c0 26.5098 21.4902 48 48 48
h172.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM266 -16c3.31152 0 6 2.68848 6 6v42h-96c-26.5098 0 -48 21.4902 -48 48v224h-74c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM394 80c3.31152 0 6 2.68848 6 6v202h-88
c-13.2549 0 -24 10.7451 -24 24v88h-106c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM400 336v9.63184v0.000976562c0 1.37207 -0.787109 3.27246 -1.75684 4.24219l-48.3682 48.3682c-1.12598 1.125 -2.65234 1.75684 -4.24316 1.75684h-9.63184
v-64h64z" />
<glyph glyph-name="save" unicode="&#xf0c7;" horiz-adv-x="448"
d="M433.941 318.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h268.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM272 368h-128v-80h128v80
zM394 16c3.31152 0 6 2.68848 6 6v259.632v0.000976562c0 1.37207 -0.787109 3.27246 -1.75684 4.24219l-78.2432 78.2432v-100.118c0 -13.2549 -10.7451 -24 -24 -24h-176c-13.2549 0 -24 10.7451 -24 24v104h-42c-3.31152 0 -6 -2.68848 -6 -6v-340
c0 -3.31152 2.68848 -6 6 -6h340zM224 216c48.5234 0 88 -39.4766 88 -88s-39.4766 -88 -88 -88s-88 39.4766 -88 88s39.4766 88 88 88zM224 88c22.0557 0 40 17.9443 40 40s-17.9443 40 -40 40s-40 -17.9443 -40 -40s17.9443 -40 40 -40z" />
<glyph glyph-name="square" unicode="&#xf0c8;" horiz-adv-x="448"
d="M400 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352zM394 16c3.2998 0 6 2.7002 6 6v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340z" />
<glyph glyph-name="envelope" unicode="&#xf0e0;"
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM464 336h-416v-40.8047c22.4248 -18.2627 58.1797 -46.6602 134.587 -106.49
c16.834 -13.2422 50.2051 -45.0762 73.4131 -44.7012c23.2119 -0.371094 56.5723 31.4541 73.4131 44.7012c76.4189 59.8389 112.165 88.2305 134.587 106.49v40.8047zM48 48h416v185.601c-22.915 -18.252 -55.4189 -43.8691 -104.947 -82.6523
c-22.5439 -17.748 -60.3359 -55.1787 -103.053 -54.9473c-42.9277 -0.231445 -81.2051 37.75 -103.062 54.9551c-49.5293 38.7842 -82.0244 64.3945 -104.938 82.6455v-185.602z" />
<glyph glyph-name="lightbulb" unicode="&#xf0eb;" horiz-adv-x="352"
d="M176 368c8.83984 0 16 -7.16016 16 -16s-7.16016 -16 -16 -16c-35.2803 0 -64 -28.7002 -64 -64c0 -8.83984 -7.16016 -16 -16 -16s-16 7.16016 -16 16c0 52.9404 43.0596 96 96 96zM96.0596 -11.1699l-0.0400391 43.1797h159.961l-0.0507812 -43.1797
c-0.00976562 -3.13965 -0.939453 -6.21973 -2.67969 -8.83984l-24.5098 -36.8398c-2.95996 -4.45996 -7.95996 -7.14062 -13.3203 -7.14062h-78.8496c-5.35059 0 -10.3506 2.68066 -13.3203 7.14062l-24.5098 36.8398c-1.75 2.62012 -2.68066 5.68945 -2.68066 8.83984z
M176 448c97.2002 0 176 -78.7998 176 -176c0 -44.3701 -16.4502 -84.8496 -43.5498 -115.79c-16.6406 -18.9795 -42.7402 -58.79 -52.4199 -92.1602v-0.0498047h-48v0.0996094c0.00390625 4.04199 0.999023 10.4482 2.21973 14.3008
c5.67969 17.9893 22.9902 64.8496 62.0996 109.46c20.4102 23.29 31.6504 53.1699 31.6504 84.1396c0 70.5801 -57.4199 128 -128 128c-68.2803 0 -128.15 -54.3604 -127.95 -128c0.0898438 -30.9902 11.0703 -60.71 31.6104 -84.1396
c39.3496 -44.9004 56.5801 -91.8604 62.1699 -109.67c1.42969 -4.56055 2.13965 -9.30078 2.15039 -14.0703v-0.120117h-48v0.0595703c-9.68066 33.3604 -35.7803 73.1709 -52.4209 92.1602c-27.1094 30.9307 -43.5596 71.4102 -43.5596 115.78
c0 93.0303 73.7197 176 176 176z" />
<glyph glyph-name="bell" unicode="&#xf0f3;" horiz-adv-x="448"
d="M439.39 85.71c6 -6.44043 8.66016 -14.1602 8.61035 -21.71c-0.0996094 -16.4004 -12.9805 -32 -32.0996 -32h-383.801c-19.1191 0 -31.9893 15.5996 -32.0996 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c19.3193 20.7598 55.4697 51.9902 55.4697 154.29
c0 77.7002 54.4795 139.9 127.939 155.16v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -102.3 36.1504 -133.53 55.4697 -154.29zM67.5303 80h312.939
c-21.2197 27.96 -44.4199 74.3203 -44.5293 159.42c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112s-112 -50.1396 -112 -112c0 -0.200195 0.0595703 -0.379883 0.0595703 -0.580078
c-0.109375 -85.0898 -23.3096 -131.45 -44.5293 -159.42zM224 -64c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
<glyph glyph-name="hospital" unicode="&#xf0f8;" horiz-adv-x="448"
d="M128 204v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12zM268 192c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40
c0 -6.62695 -5.37305 -12 -12 -12h-40zM192 108c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40zM268 96c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40
c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.62695 5.37305 12 12 12h19.5v378.965c0 11.6172 10.7451 21.0352 24 21.0352h88.5v40c0 13.2549 10.7451 24 24 24h112c13.2549 0 24 -10.7451 24 -24v-40h88.5
c13.2549 0 24 -9.41797 24 -21.0352v-378.965h19.5c6.62695 0 12 -5.37305 12 -12zM79.5 -15h112.5v67c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-67h112.5v351h-64.5v-24c0 -13.2549 -10.7451 -24 -24 -24h-112c-13.2549 0 -24 10.7451 -24 24v24
h-64.5v-351zM266 384h-26v26c0 3.31152 -2.68848 6 -6 6h-20c-3.31152 0 -6 -2.68848 -6 -6v-26h-26c-3.31152 0 -6 -2.68848 -6 -6v-20c0 -3.31152 2.68848 -6 6 -6h26v-26c0 -3.31152 2.68848 -6 6 -6h20c3.31152 0 6 2.68848 6 6v26h26c3.31152 0 6 2.68848 6 6v20
c0 3.31152 -2.68848 6 -6 6z" />
<glyph glyph-name="plus-square" unicode="&#xf0fe;" horiz-adv-x="448"
d="M352 208v-32c0 -6.59961 -5.40039 -12 -12 -12h-88v-88c0 -6.59961 -5.40039 -12 -12 -12h-32c-6.59961 0 -12 5.40039 -12 12v88h-88c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h88v88c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12
v-88h88c6.59961 0 12 -5.40039 12 -12zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340
c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="circle" unicode="&#xf111;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200z" />
<glyph glyph-name="smile" unicode="&#xf118;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM332 135.4c8.5 10.1992 23.7002 11.5 33.7998 3.09961c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998
c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.0996 8.40039 25.2998 7.09961 33.7998 -3.09961c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004z" />
<glyph glyph-name="frown" unicode="&#xf119;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 144c40.2002 0 78 -17.7002 103.8 -48.5996c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008
c-10.7002 -8.7998 -25.7002 -6.59961 -33.7998 3.10059c-16.6006 20 -41 31.3994 -66.9004 31.3994s-50.2998 -11.5 -66.9004 -31.3994c-8.5 -10.2002 -23.5996 -11.5 -33.7998 -3.10059c-10.2002 8.5 -11.5996 23.6006 -3.09961 33.8008
c25.7998 30.8994 63.5996 48.5996 103.8 48.5996z" />
<glyph glyph-name="meh" unicode="&#xf11a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM336 128c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-176c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h176z
" />
<glyph glyph-name="keyboard" unicode="&#xf11c;" horiz-adv-x="576"
d="M528 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h480zM536 48v288c0 4.41113 -3.58887 8 -8 8h-480c-4.41113 0 -8 -3.58887 -8 -8v-288c0 -4.41113 3.58887 -8 8 -8
h480c4.41113 0 8 3.58887 8 8zM170 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM266 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM362 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM458 178c0 -6.62695 -5.37305 -12 -12 -12h-28
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 96c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 96
c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28
c6.62695 0 12 -5.37305 12 -12v-28zM218 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM314 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM410 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 260c0 -6.62695 -5.37305 -12 -12 -12h-28
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM408 102c0 -6.62695 -5.37305 -12 -12 -12h-216c-6.62695 0 -12 5.37305 -12 12v16c0 6.62695 5.37305 12 12 12h216c6.62695 0 12 -5.37305 12 -12v-16z" />
<glyph glyph-name="calendar" unicode="&#xf133;" horiz-adv-x="448"
d="M400 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12
v-52h48zM394 -16c3.2998 0 6 2.7002 6 6v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340z" />
<glyph glyph-name="play-circle" unicode="&#xf144;"
d="M371.7 210c16.3994 -9.2002 16.3994 -32.9004 0 -42l-176 -101c-15.9004 -8.7998 -35.7002 2.59961 -35.7002 21v208c0 18.5 19.9004 29.7998 35.7002 21zM504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192
c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200z" />
<glyph glyph-name="minus-square" unicode="&#xf146;" horiz-adv-x="448"
d="M108 164c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h232c6.59961 0 12 -5.40039 12 -12v-32c0 -6.59961 -5.40039 -12 -12 -12h-232zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352
c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="check-square" unicode="&#xf14a;" horiz-adv-x="448"
d="M400 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h352zM400 16v352h-352v-352h352zM364.136 257.724l-172.589 -171.204
c-4.70508 -4.66699 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66699 4.70508 -4.63672 12.3037 0.0693359 16.9717l22.7188 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l59.792 -60.2773l141.353 140.217
c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0683594l22.5361 -22.7178c4.66699 -4.70605 4.63672 -12.3047 -0.0683594 -16.9717z" />
<glyph glyph-name="share-square" unicode="&#xf14d;" horiz-adv-x="576"
d="M561.938 289.94c18.75 -18.7402 18.75 -49.1406 0 -67.8809l-143.998 -144c-29.9727 -29.9727 -81.9404 -9.05273 -81.9404 33.9404v53.7998c-101.266 -7.83691 -99.625 -31.6406 -84.1104 -78.7598c14.2285 -43.0889 -33.4736 -79.248 -71.0195 -55.7402
c-51.6924 32.3057 -84.8701 83.0635 -84.8701 144.76c0 39.3408 12.2197 72.7402 36.3301 99.3008c19.8398 21.8398 47.7402 38.4697 82.9102 49.4199c36.7295 11.4395 78.3096 16.1094 120.76 17.9893v57.1982c0 42.9355 51.9258 63.9541 81.9404 33.9404zM384 112l144 144
l-144 144v-104.09c-110.86 -0.90332 -240 -10.5166 -240 -119.851c0 -52.1396 32.79 -85.6094 62.3096 -104.06c-39.8174 120.65 48.999 141.918 177.69 143.84v-103.84zM408.74 27.5068c6.14844 1.75684 15.5449 5.92383 20.9736 9.30273
c7.97656 4.95215 18.2861 -0.825195 18.2861 -10.2139v-42.5957c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h132c6.62695 0 12 -5.37305 12 -12v-4.48633c0 -4.91699 -2.9873 -9.36914 -7.56934 -11.1514
c-13.7021 -5.33105 -26.3955 -11.5371 -38.0498 -18.585c-1.59668 -0.974609 -4.41016 -1.77051 -6.28027 -1.77734h-86.1006c-3.31152 0 -6 -2.68848 -6 -6v-340c0 -3.31152 2.68848 -6 6 -6h340c3.31152 0 6 2.68848 6 6v25.9658c0 5.37012 3.5791 10.0596 8.74023 11.541
z" />
<glyph glyph-name="compass" unicode="&#xf14e;" horiz-adv-x="496"
d="M347.94 318.14c16.6592 7.61035 33.8096 -9.54004 26.1992 -26.1992l-65.9697 -144.341c-2.73047 -5.97363 -9.7959 -13.0391 -15.7695 -15.7695l-144.341 -65.9697c-16.6592 -7.61035 -33.8096 9.5498 -26.1992 26.1992l65.9697 144.341
c2.73047 5.97363 9.7959 13.0391 15.7695 15.7695zM270.58 169.42c12.4697 12.4697 12.4697 32.6904 0 45.1602s-32.6904 12.4697 -45.1602 0s-12.4697 -32.6904 0 -45.1602s32.6904 -12.4697 45.1602 0zM248 440c136.97 0 248 -111.03 248 -248s-111.03 -248 -248 -248
s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
<glyph glyph-name="caret-square-down" unicode="&#xf150;" horiz-adv-x="448"
d="M125.1 240h197.801c10.6992 0 16.0996 -13 8.5 -20.5l-98.9004 -98.2998c-4.7002 -4.7002 -12.2002 -4.7002 -16.9004 0l-98.8994 98.2998c-7.7002 7.5 -2.2998 20.5 8.39941 20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="caret-square-up" unicode="&#xf151;" horiz-adv-x="448"
d="M322.9 144h-197.801c-10.6992 0 -16.0996 13 -8.5 20.5l98.9004 98.2998c4.7002 4.7002 12.2002 4.7002 16.9004 0l98.8994 -98.2998c7.7002 -7.5 2.2998 -20.5 -8.39941 -20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="caret-square-right" unicode="&#xf152;" horiz-adv-x="448"
d="M176 93.0996v197.801c0 10.6992 13 16.0996 20.5 8.5l98.2998 -98.9004c4.7002 -4.7002 4.7002 -12.2002 0 -16.9004l-98.2998 -98.8994c-7.5 -7.7002 -20.5 -2.2998 -20.5 8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="file" unicode="&#xf15b;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416z" />
<glyph glyph-name="file-alt" unicode="&#xf15c;" horiz-adv-x="384"
d="M288 200v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v28c0 6.59961 5.40039 12 12 12h168c6.59961 0 12 -5.40039 12 -12zM276 128c6.59961 0 12 -5.40039 12 -12v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12
v28c0 6.59961 5.40039 12 12 12h168zM384 316.1v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996l83.9004 -83.9004c9 -8.90039 14.0996 -21.2002 14.0996 -33.9004z
M256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288z" />
<glyph glyph-name="thumbs-up" unicode="&#xf164;"
d="M466.27 161.31c4.6748 -22.6465 0.864258 -44.5371 -8.98926 -62.9893c2.95898 -23.8682 -4.02148 -48.5654 -17.3398 -66.9902c-0.954102 -55.9072 -35.8232 -95.3301 -112.94 -95.3301c-7 0 -15 0.00976562 -22.2197 0.00976562
c-102.742 0 -133.293 38.9395 -177.803 39.9404c-3.56934 -13.7764 -16.085 -23.9502 -30.9775 -23.9502h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h98.7598c19.1455 16.9531 46.0137 60.6533 68.7598 83.4004
c13.667 13.667 10.1533 108.6 71.7607 108.6c57.5801 0 95.2695 -31.9355 95.2695 -104.73c0 -18.4092 -3.92969 -33.7295 -8.84961 -46.5391h36.4795c48.6025 0 85.8203 -41.5654 85.8203 -85.5801c0 -19.1504 -4.95996 -34.9902 -13.7305 -49.8408zM404.52 107.48
c21.5811 20.3838 18.6992 51.0645 5.21094 65.6191c9.44922 0 22.3594 18.9102 22.2695 37.8105c-0.0898438 18.9102 -16.71 37.8203 -37.8203 37.8203h-103.989c0 37.8193 28.3594 55.3691 28.3594 94.5391c0 23.75 0 56.7305 -47.2695 56.7305
c-18.9102 -18.9102 -9.45996 -66.1797 -37.8203 -94.54c-26.5596 -26.5703 -66.1797 -97.46 -94.54 -97.46h-10.9199v-186.17c53.6113 0 100.001 -37.8203 171.64 -37.8203h37.8203c35.5117 0 60.8203 17.1201 53.1201 65.9004
c15.2002 8.16016 26.5 36.4395 13.9395 57.5703zM88 16c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
<glyph glyph-name="thumbs-down" unicode="&#xf165;"
d="M466.27 222.69c8.77051 -14.8506 13.7305 -30.6904 13.7305 -49.8408c0 -44.0146 -37.2178 -85.5801 -85.8203 -85.5801h-36.4795c4.91992 -12.8096 8.84961 -28.1299 8.84961 -46.5391c0 -72.7949 -37.6895 -104.73 -95.2695 -104.73
c-61.6074 0 -58.0938 94.9326 -71.7607 108.6c-22.7461 22.7471 -49.6133 66.4473 -68.7598 83.4004h-7.05176c-5.5332 -9.56152 -15.8662 -16 -27.708 -16h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h64c8.11328 0 15.5146 -3.02539 21.1553 -8
h10.8447c40.9971 0 73.1953 39.9902 176.78 39.9902c7.21973 0 15.2197 0.00976562 22.2197 0.00976562c77.1172 0 111.986 -39.4229 112.94 -95.3301c13.3184 -18.4248 20.2979 -43.1221 17.3398 -66.9902c9.85352 -18.4521 13.6641 -40.3428 8.98926 -62.9893zM64 152
c13.2549 0 24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24zM394.18 135.27c21.1104 0 37.7305 18.9102 37.8203 37.8203c0.0898438 18.9004 -12.8203 37.8105 -22.2695 37.8105c13.4883 14.5547 16.3701 45.2354 -5.21094 65.6191
c12.5605 21.1309 1.26074 49.4102 -13.9395 57.5703c7.7002 48.7803 -17.6084 65.9004 -53.1201 65.9004h-37.8203c-71.6387 0 -118.028 -37.8203 -171.64 -37.8203v-186.17h10.9199c28.3604 0 67.9805 -70.8896 94.54 -97.46
c28.3604 -28.3604 18.9102 -75.6299 37.8203 -94.54c47.2695 0 47.2695 32.9805 47.2695 56.7305c0 39.1699 -28.3594 56.7197 -28.3594 94.5391h103.989z" />
<glyph glyph-name="sun" unicode="&#xf185;"
d="M494.2 226.1c11.2002 -7.59961 17.7998 -20.0996 17.8994 -33.6992c0 -13.4004 -6.69922 -26 -17.7998 -33.5l-59.7998 -40.5l13.7002 -71c2.5 -13.2002 -1.60059 -26.8008 -11.1006 -36.3008s-22.8994 -13.7998 -36.2998 -11.0996l-70.8994 13.7002l-40.4004 -59.9004
c-7.5 -11.0996 -20.0996 -17.7998 -33.5 -17.7998s-26 6.7002 -33.5 17.9004l-40.4004 59.8994l-70.7998 -13.7002c-13.3994 -2.59961 -26.7998 1.60059 -36.2998 11.1006s-13.7002 23.0996 -11.0996 36.2998l13.6992 71l-59.7998 40.5
c-11.0996 7.5 -17.7998 20 -17.7998 33.5s6.59961 26 17.7998 33.5996l59.7998 40.5l-13.6992 71c-2.60059 13.2002 1.59961 26.7002 11.0996 36.3008c9.5 9.59961 23 13.6992 36.2998 11.1992l70.7998 -13.6992l40.4004 59.8994c15.0996 22.2998 51.9004 22.2998 67 0
l40.4004 -59.8994l70.8994 13.6992c13 2.60059 26.6006 -1.59961 36.2002 -11.0996c9.5 -9.59961 13.7002 -23.2002 11.0996 -36.4004l-13.6992 -71zM381.3 140.5l76.7998 52.0996l-76.7998 52l17.6006 91.1006l-91 -17.6006l-51.9004 76.9004l-51.7998 -76.7998
l-91 17.5996l17.5996 -91.2002l-76.7998 -52l76.7998 -52l-17.5996 -91.1992l90.8994 17.5996l51.9004 -77l51.9004 76.9004l91 -17.6006zM256 296c57.2998 0 104 -46.7002 104 -104s-46.7002 -104 -104 -104s-104 46.7002 -104 104s46.7002 104 104 104zM256 136
c30.9004 0 56 25.0996 56 56s-25.0996 56 -56 56s-56 -25.0996 -56 -56s25.0996 -56 56 -56z" />
<glyph glyph-name="moon" unicode="&#xf186;"
d="M279.135 -64c-141.424 0 -256 114.64 -256 256c0 141.425 114.641 256 256 256c13.0068 -0.00195312 33.9443 -1.91797 46.7354 -4.27734c44.0205 -8.13086 53.7666 -66.8691 15.0215 -88.9189c-41.374 -23.5439 -67.4336 -67.4121 -67.4336 -115.836
c0 -83.5234 75.9238 -146.475 158.272 -130.792c43.6904 8.32129 74.5186 -42.5693 46.248 -77.4004c-47.8613 -58.9717 -120.088 -94.7754 -198.844 -94.7754zM279.135 400c-114.875 0 -208 -93.125 -208 -208s93.125 -208 208 -208
c65.2314 0 123.439 30.0361 161.575 77.0244c-111.611 -21.2568 -215.252 64.0957 -215.252 177.943c0 67.5127 36.9326 126.392 91.6934 157.555c-12.3271 2.27637 -25.0312 3.47754 -38.0166 3.47754z" />
<glyph glyph-name="caret-square-left" unicode="&#xf191;" horiz-adv-x="448"
d="M272 290.9v-197.801c0 -10.6992 -13 -16.0996 -20.5 -8.5l-98.2998 98.9004c-4.7002 4.7002 -4.7002 12.2002 0 16.9004l98.2998 98.8994c7.5 7.7002 20.5 2.2998 20.5 -8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="dot-circle" unicode="&#xf192;"
d="M256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200zM256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248z
M256 272c44.1826 0 80 -35.8174 80 -80s-35.8174 -80 -80 -80s-80 35.8174 -80 80s35.8174 80 80 80z" />
<glyph glyph-name="building" unicode="&#xf1ad;" horiz-adv-x="448"
d="M128 300v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12zM268 288c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40
c0 -6.59961 -5.40039 -12 -12 -12h-40zM140 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM268 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM192 108c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM268 96c-6.59961 0 -12 5.40039 -12 12v40
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.59961 5.40039 12 12 12h19.5v440c0 13.2998 10.7002 24 24 24h337c13.2998 0 24 -10.7002 24 -24v-440h19.5
c6.59961 0 12 -5.40039 12 -12zM79.5 -15h112.5v67c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-67h112.5v414l-288.5 1z" />
<glyph glyph-name="file-pdf" unicode="&#xf1c1;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM298.2 127.7c10.5 -10.5 8 -38.7002 -17.5 -38.7002c-14.7998 0 -36.9004 6.7998 -55.7998 17c-21.6006 -3.59961 -46 -12.7002 -68.4004 -20.0996c-50.0996 -86.4004 -79.4004 -47 -76.0996 -31.2002
c4 20 31 35.8994 51 46.2002c10.5 18.3994 25.3994 50.5 35.3994 74.3994c-7.39941 28.6006 -11.3994 51 -7 67.1006c4.7998 17.6992 38.4004 20.2998 42.6006 -5.90039c4.69922 -15.4004 -1.5 -39.9004 -5.40039 -56c8.09961 -21.2998 19.5996 -35.7998 36.7998 -46.2998
c17.4004 2.2002 52.2002 5.5 64.4004 -6.5zM100.1 49.9004c0 -0.700195 11.4004 4.69922 30.4004 35c-5.90039 -5.5 -25.2998 -21.3008 -30.4004 -35zM181.7 240.5c-2.5 0 -2.60059 -26.9004 1.7998 -40.7998c4.90039 8.7002 5.59961 40.7998 -1.7998 40.7998zM157.3 103.9
c15.9004 6.09961 34 14.8994 54.7998 19.1992c-11.1992 8.30078 -21.7998 20.4004 -30.0996 35.5c-6.7002 -17.6992 -15 -37.7998 -24.7002 -54.6992zM288.9 108.9c3.59961 2.39941 -2.2002 10.3994 -37.3008 7.7998c32.3008 -13.7998 37.3008 -7.7998 37.3008 -7.7998z" />
<glyph glyph-name="file-word" unicode="&#xf1c2;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM268.1 192v0.200195h15.8008c7.7998 0 13.5 -7.2998 11.5996 -14.9004c-4.2998 -17 -13.7002 -54.0996 -34.5 -136c-1.2998 -5.39941 -6.09961 -9.09961 -11.5996 -9.09961h-24.7002
c-5.5 0 -10.2998 3.7998 -11.6006 9.09961c-5.2998 20.9004 -17.7998 71 -17.8994 71.4004l-2.90039 17.2998c-0.5 -5.2998 -1.5 -11.0996 -3 -17.2998l-17.8994 -71.4004c-1.30078 -5.39941 -6.10059 -9.09961 -11.6006 -9.09961h-25.2002
c-5.59961 0 -10.3994 3.7002 -11.6992 9.09961c-6.5 26.5 -25.2002 103.4 -33.2002 136c-1.7998 7.5 3.89941 14.7998 11.7002 14.7998h16.7998c5.7998 0 10.7002 -4.09961 11.7998 -9.69922c5 -25.7002 18.4004 -93.8008 19.0996 -99
c0.300781 -1.7002 0.400391 -3.10059 0.5 -4.2002c0.800781 7.5 0.400391 4.7002 24.8008 103.7c1.39941 5.2998 6.19922 9.09961 11.6992 9.09961h13.3008c5.59961 0 10.3994 -3.7998 11.6992 -9.2002c23.9004 -99.7002 22.8008 -94.3994 23.6006 -99.5
c0.299805 -1.7002 0.5 -3.09961 0.700195 -4.2998c0.599609 8.09961 0.399414 5.7998 21 103.5c1.09961 5.5 6 9.5 11.6992 9.5z" />
<glyph glyph-name="file-excel" unicode="&#xf1c3;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM260 224c9.2002 0 15 -10 10.2998 -18c-16 -27.5 -45.5996 -76.9004 -46.2998 -78l46.4004 -78c4.59961 -8 -1.10059 -18 -10.4004 -18h-28.7998c-4.40039 0 -8.5 2.40039 -10.6006 6.2998
c-22.6992 41.7998 -13.6992 27.5 -28.5996 57.7002c-5.59961 -12.7002 -6.90039 -17.7002 -28.5996 -57.7002c-2.10059 -3.89941 -6.10059 -6.2998 -10.5 -6.2998h-28.9004c-9.2998 0 -15.0996 10 -10.4004 18l46.3008 78l-46.3008 78c-4.59961 8 1.10059 18 10.4004 18
h28.9004c4.39941 0 8.5 -2.40039 10.5996 -6.2998c21.7002 -40.4004 14.7002 -28.6006 28.5996 -57.7002c6.40039 15.2998 10.6006 24.5996 28.6006 57.7002c2.09961 3.89941 6.09961 6.2998 10.5 6.2998h28.7998z" />
<glyph glyph-name="file-powerpoint" unicode="&#xf1c4;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM120 44v168c0 6.59961 5.40039 12 12 12h69.2002c36.7002 0 62.7998 -27 62.7998 -66.2998c0 -74.2998 -68.7002 -66.5 -95.5 -66.5v-47.2002c0 -6.59961 -5.40039 -12 -12 -12h-24.5c-6.59961 0 -12 5.40039 -12 12z
M168.5 131.4h23c7.90039 0 13.9004 2.39941 18.0996 7.19922c8.5 9.80078 8.40039 28.5 0.100586 37.8008c-4.10059 4.59961 -9.90039 7 -17.4004 7h-23.8994v-52h0.0996094z" />
<glyph glyph-name="file-image" unicode="&#xf1c5;" horiz-adv-x="384"
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM80 32v64l39.5 39.5c4.7002 4.7002 12.2998 4.7002 17 0l39.5 -39.5l87.5 87.5c4.7002 4.7002 12.2998 4.7002 17 0l23.5 -23.5v-128h-224zM128 272c26.5 0 48 -21.5 48 -48s-21.5 -48 -48 -48s-48 21.5 -48 48
s21.5 48 48 48z" />
<glyph glyph-name="file-archive" unicode="&#xf1c6;" horiz-adv-x="384"
d="M128.3 288h32v-32h-32v32zM192.3 384v-32h-32v32h32zM128.3 352h32v-32h-32v32zM192.3 320v-32h-32v32h32zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1
c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-48.2998v-16h-32v16h-79.7002v-416h288zM194.2 182.3l17.2998 -87.7002c6.40039 -32.3994 -18.4004 -62.5996 -51.5 -62.5996
c-33.2002 0 -58 30.4004 -51.4004 62.9004l19.7002 97.0996v32h32v-32h22.1006c5.7998 0 10.6992 -4.09961 11.7998 -9.7002zM160.3 57.9004c17.9004 0 32.4004 12.0996 32.4004 27c0 14.8994 -14.5 27 -32.4004 27c-17.8994 0 -32.3994 -12.1006 -32.3994 -27
c0 -14.9004 14.5 -27 32.3994 -27zM192.3 256v-32h-32v32h32z" />
<glyph glyph-name="file-audio" unicode="&#xf1c7;" horiz-adv-x="384"
d="M369.941 350.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM332.118 320
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM192 60.0244c0 -10.6914 -12.9258 -16.0459 -20.4854 -8.48535l-35.5146 35.9746h-28c-6.62695 0 -12 5.37305 -12 12v56c0 6.62695 5.37305 12 12 12h28
l35.5146 36.9473c7.56055 7.56055 20.4854 2.20605 20.4854 -8.48535v-135.951zM233.201 107.154c9.05078 9.29688 9.05957 24.1328 0.000976562 33.4385c-22.1494 22.752 12.2344 56.2461 34.3945 33.4814c27.1982 -27.9404 27.2119 -72.4443 0.000976562 -100.401
c-21.793 -22.3857 -56.9463 10.3154 -34.3965 33.4814z" />
<glyph glyph-name="file-video" unicode="&#xf1c8;" horiz-adv-x="384"
d="M369.941 350.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM332.118 320
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM276.687 195.303c10.0049 10.0049 27.3135 2.99707 27.3135 -11.3135v-111.976c0 -14.2939 -17.2959 -21.332 -27.3135 -11.3135l-52.6865 52.6738v-37.374
c0 -11.0459 -8.9541 -20 -20 -20h-104c-11.0459 0 -20 8.9541 -20 20v104c0 11.0459 8.9541 20 20 20h104c11.0459 0 20 -8.9541 20 -20v-37.374z" />
<glyph glyph-name="file-code" unicode="&#xf1c9;" horiz-adv-x="384"
d="M149.9 98.9004c3.5 -3.30078 3.69922 -8.90039 0.399414 -12.4004l-17.3994 -18.5996c-1.60059 -1.80078 -4 -2.80078 -6.40039 -2.80078c-2.2002 0 -4.40039 0.900391 -6 2.40039l-57.7002 54.0996c-3.7002 3.40039 -3.7002 9.30078 0 12.8008l57.7002 54.0996
c3.40039 3.2998 9 3.2002 12.4004 -0.400391l17.3994 -18.5996l0.200195 -0.200195c3.2002 -3.59961 2.7998 -9.2002 -0.799805 -12.3994l-32.7998 -28.9004l32.7998 -28.9004zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288
c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288zM209.6 234l24.4004 -7
c4.7002 -1.2998 7.40039 -6.2002 6 -10.9004l-54.7002 -188.199c-1.2998 -4.60059 -6.2002 -7.40039 -10.8994 -6l-24.4004 7.09961c-4.7002 1.2998 -7.40039 6.2002 -6 10.9004l54.7002 188.1c1.39941 4.7002 6.2002 7.40039 10.8994 6zM234.1 157.1
c-3.5 3.30078 -3.69922 8.90039 -0.399414 12.4004l17.3994 18.5996c3.30078 3.60059 8.90039 3.7002 12.4004 0.400391l57.7002 -54.0996c3.7002 -3.40039 3.7002 -9.30078 0 -12.8008l-57.7002 -54.0996c-3.5 -3.2998 -9.09961 -3.09961 -12.4004 0.400391
l-17.3994 18.5996l-0.200195 0.200195c-3.2002 3.59961 -2.7998 9.2002 0.799805 12.3994l32.7998 28.9004l-32.7998 28.9004z" />
<glyph glyph-name="life-ring" unicode="&#xf1cd;"
d="M256 -56c-136.967 0 -248 111.033 -248 248s111.033 248 248 248s248 -111.033 248 -248s-111.033 -248 -248 -248zM152.602 20.7197c63.2178 -38.3184 143.579 -38.3184 206.797 0l-53.4111 53.4111c-31.8467 -13.5215 -68.168 -13.5059 -99.9746 0zM336 192
c0 44.1123 -35.8877 80 -80 80s-80 -35.8877 -80 -80s35.8877 -80 80 -80s80 35.8877 80 80zM427.28 88.6016c38.3184 63.2178 38.3184 143.579 0 206.797l-53.4111 -53.4111c13.5215 -31.8467 13.5049 -68.168 0 -99.9746zM359.397 363.28
c-63.2168 38.3184 -143.578 38.3184 -206.796 0l53.4111 -53.4111c31.8457 13.5215 68.167 13.5049 99.9736 0zM84.7197 295.398c-38.3184 -63.2178 -38.3184 -143.579 0 -206.797l53.4111 53.4111c-13.5215 31.8467 -13.5059 68.168 0 99.9746z" />
<glyph glyph-name="paper-plane" unicode="&#xf1d8;"
d="M440 441.5c34.5996 19.9004 77.5996 -8.7998 71.5 -48.9004l-59.4004 -387.199c-2.2998 -14.5 -11.0996 -27.3008 -23.8994 -34.5c-7.2998 -4.10059 -15.4004 -6.2002 -23.6006 -6.2002c-6.19922 0 -12.3994 1.2002 -18.2998 3.59961l-111.899 46.2002l-43.8008 -59.0996
c-27.3994 -36.9004 -86.5996 -17.8008 -86.5996 28.5996v84.4004l-114.3 47.2998c-36.7998 15.0996 -40.1006 66 -5.7002 85.8994zM192 -16l36.5996 49.5l-36.5996 15.0996v-64.5996zM404.6 12.7002l59.4004 387.3l-416 -240l107.8 -44.5996l211.5 184.3
c14.2002 12.2998 34.4004 -5.7002 23.7002 -21.2002l-140.2 -202.3z" />
<glyph glyph-name="futbol" unicode="&#xf1e3;" horiz-adv-x="496"
d="M483.8 268.6c42.2998 -130.199 -29 -270.1 -159.2 -312.399c-25.5 -8.2998 -51.2998 -12.2002 -76.6992 -12.2002c-104.5 0 -201.7 66.5996 -235.7 171.4c-42.2998 130.199 29 270.1 159.2 312.399c25.5 8.2998 51.2998 12.2002 76.6992 12.2002
c104.5 0 201.7 -66.5996 235.7 -171.4zM409.3 74.9004c6.10059 8.39941 12.1006 16.8994 16.7998 26.1992c14.3008 28.1006 21.5 58.5 21.7002 89.2002l-38.8994 36.4004l-71.1006 -22.1006l-24.3994 -75.1992l43.6992 -60.9004zM409.3 310.3
c-24.5 33.4004 -58.7002 58.4004 -97.8994 71.4004l-47.4004 -26.2002v-73.7998l64.2002 -46.5l70.7002 22zM184.9 381.6c-39.9004 -13.2998 -73.5 -38.5 -97.8008 -71.8994l10.1006 -52.5l70.5996 -22l64.2002 46.5v73.7998zM139 68.5l43.5 61.7002l-24.2998 74.2998
l-71.1006 22.2002l-39 -36.4004c0.5 -55.7002 23.4004 -95.2002 37.8008 -115.3zM187.2 1.5c64.0996 -20.4004 115.5 -1.7998 121.7 0l22.3994 48.0996l-44.2998 61.7002h-78.5996l-43.6006 -61.7002z" />
<glyph glyph-name="newspaper" unicode="&#xf1ea;" horiz-adv-x="576"
d="M552 384c13.2549 0 24 -10.7451 24 -24v-336c0 -13.2549 -10.7451 -24 -24 -24h-496c-30.9277 0 -56 25.0723 -56 56v272c0 13.2549 10.7451 24 24 24h42.752c6.60547 18.623 24.3896 32 45.248 32h440zM48 56c0 -4.41113 3.58887 -8 8 -8s8 3.58887 8 8v248h-16v-248z
M528 48v288h-416v-280c0 -2.7168 -0.204102 -5.38574 -0.578125 -8h416.578zM172 168c-6.62695 0 -12 5.37305 -12 12v96c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-96c0 -6.62695 -5.37305 -12 -12 -12h-136zM200 248v-40h80v40h-80zM160 108v24
c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-136c-6.62695 0 -12 5.37305 -12 12zM352 108v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104
c-6.62695 0 -12 5.37305 -12 12zM352 252v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12zM352 180v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24
c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12z" />
<glyph glyph-name="bell-slash" unicode="&#xf1f6;" horiz-adv-x="640"
d="M633.99 -23.0195c6.91016 -5.52051 8.01953 -15.5908 2.5 -22.4902l-10 -12.4902c-5.53027 -6.88965 -15.5898 -8.00977 -22.4902 -2.49023l-598 467.51c-6.90039 5.52051 -8.01953 15.5908 -2.49023 22.4902l10 12.4902
c5.52051 6.90039 15.5898 8.00977 22.4902 2.49023zM163.53 80h182.84l61.3994 -48h-279.659c-19.1201 0 -31.9902 15.5996 -32.1006 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c18.3701 19.7402 51.5703 49.6904 54.8398 140.42l45.4697 -35.5498
c-6.91992 -54.7803 -24.6895 -88.5498 -41.3994 -110.58zM320 352c-23.3496 0 -45 -7.17969 -62.9404 -19.4004l-38.1699 29.8408c19.6807 15.7793 43.1104 27.3096 69.1299 32.7197v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398
c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -41.3604 6.03027 -70.7197 14.3398 -92.8496l-59.5293 46.54c-1.63086 13.96 -2.77051 28.8896 -2.79004 45.7295c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112zM320 -64
c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
<glyph glyph-name="copyright" unicode="&#xf1f9;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
M363.351 93.0645c-9.61328 -9.71289 -45.5293 -41.3965 -104.064 -41.3965c-82.4297 0 -140.484 61.4248 -140.484 141.567c0 79.1514 60.2754 139.4 139.763 139.4c55.5303 0 88.7373 -26.6201 97.5928 -34.7783c2.13379 -1.96289 3.86523 -5.9082 3.86523 -8.80762
c0 -1.95508 -0.864258 -4.87402 -1.92969 -6.51465l-18.1543 -28.1133c-3.8418 -5.9502 -11.9668 -7.28223 -17.499 -2.9209c-8.5957 6.77637 -31.8145 22.5381 -61.708 22.5381c-48.3037 0 -77.916 -35.3301 -77.916 -80.082c0 -41.5889 26.8877 -83.6924 78.2764 -83.6924
c32.6572 0 56.8428 19.0391 65.7266 27.2256c5.26953 4.85645 13.5957 4.03906 17.8193 -1.73828l19.8652 -27.1699c1.28613 -1.74512 2.33008 -4.91992 2.33008 -7.08789c0 -2.72363 -1.56055 -6.5 -3.48242 -8.42969z" />
<glyph glyph-name="closed-captioning" unicode="&#xf20a;"
d="M464 384c26.5 0 48 -21.5 48 -48v-288c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h416zM458 48c3.2998 0 6 2.7002 6 6v276c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-276c0 -3.2998 2.7002 -6 6 -6h404z
M246.9 133.7c1.69922 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.6006 -56.7998 -172.801 -32.0996 -172.801 67.9004c0 97.2998 121.7 119.5 172.5 70.0996c2.10059 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.89941 -3.09961 -6.19922 -4 -9.09961 -1.7002
c-40.7998 32 -94.5996 14.9004 -94.5996 -31.1992c0 -48 51 -70.5 92.1992 -32.6006c2.80078 2.5 7.10059 2.10059 9.2002 -0.899414zM437.3 133.7c1.7002 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.5996 -56.9004 -172.8 -32.0996 -172.8 67.9004
c0 97.2998 121.7 119.5 172.5 70.0996c2.09961 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.90039 -3.09961 -6.2002 -4 -9.09961 -1.7002c-40.8008 32 -94.6006 14.9004 -94.6006 -31.1992c0 -48 51 -70.5 92.2002 -32.6006c2.7998 2.5 7.09961 2.10059 9.2002 -0.899414z
" />
<glyph glyph-name="object-group" unicode="&#xf247;"
d="M500 320h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v256h-12
c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM448 384v-32h32v32h-32zM32 384v-32h32v32h-32zM64 0v32
h-32v-32h32zM480 0v32h-32v-32h32zM440 64v256h-12c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h12zM404 256c6.62695 0 12 -5.37207 12 -12v-168
c0 -6.62793 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37207 -12 12v52h-84c-6.62695 0 -12 5.37207 -12 12v168c0 6.62793 5.37305 12 12 12h200c6.62695 0 12 -5.37207 12 -12v-52h84zM136 280v-112h144v112h-144zM376 104v112h-56v-76
c0 -6.62793 -5.37305 -12 -12 -12h-76v-24h144z" />
<glyph glyph-name="object-ungroup" unicode="&#xf248;" horiz-adv-x="576"
d="M564 224h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v24h-88v-12
c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h72
c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-12v-24h88v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM352 384v-32h32v32h-32zM352 128v-32h32v32h-32zM64 96v32h-32v-32h32zM64 352v32
h-32v-32h32zM96 136h224v12c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-12zM224 0v32h-32v-32h32zM504 64v160h-12c-6.62695 0 -12 5.37305 -12 12v12
h-88v-88h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-88v-24h12c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h12zM544 0v32h-32v-32h32zM544 256v32h-32v-32h32z" />
<glyph glyph-name="sticky-note" unicode="&#xf249;" horiz-adv-x="448"
d="M448 99.8936c0 -10.9746 -6.29883 -26.1797 -14.0586 -33.9404l-83.8828 -83.8818c-7.75977 -7.76074 -22.9658 -14.0596 -33.9404 -14.0596h-268.118c-26.5098 0 -48 21.4902 -48 48v351.988c0 26.5098 21.4902 48 48 48h352c26.5098 0 48 -21.4902 48 -48v-268.106z
M320 19.8936l76.1182 76.1182h-76.1182v-76.1182zM400 368h-352v-351.988h224v104c0 13.2549 10.7451 24 24 24h104v223.988z" />
<glyph glyph-name="clone" unicode="&#xf24d;"
d="M464 448c26.5098 0 48 -21.4902 48 -48v-320c0 -26.5098 -21.4902 -48 -48 -48h-48v-48c0 -26.5098 -21.4902 -48 -48 -48h-320c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h48v48c0 26.5098 21.4902 48 48 48h320zM362 -16c3.31152 0 6 2.68848 6 6
v42h-224c-26.5098 0 -48 21.4902 -48 48v224h-42c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308zM458 80c3.31152 0 6 2.68848 6 6v308c0 3.31152 -2.68848 6 -6 6h-308c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308z" />
<glyph glyph-name="hourglass" unicode="&#xf254;" horiz-adv-x="384"
d="M368 400c0 -80.0996 -31.8984 -165.619 -97.1797 -208c64.9912 -42.1934 97.1797 -127.436 97.1797 -208h4c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-360c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h4
c0 80.0996 31.8994 165.619 97.1797 208c-64.9912 42.1934 -97.1797 127.436 -97.1797 208h-4c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h360c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-4zM64 400
c0 -101.621 57.3066 -184 128 -184s128 82.3799 128 184h-256zM320 -16c0 101.62 -57.3076 184 -128 184s-128 -82.3799 -128 -184h256z" />
<glyph glyph-name="hand-rock" unicode="&#xf255;"
d="M408.864 368.948c48.8213 20.751 103.136 -15.0723 103.136 -67.9111v-114.443c0 -15.3955 -3.08887 -30.3906 -9.18262 -44.5674l-42.835 -99.6562c-4.99707 -11.625 -3.98242 -18.8574 -3.98242 -42.3701c0 -17.6729 -14.3271 -32 -32 -32h-252
c-17.6729 0 -32 14.3271 -32 32c0 27.3301 1.1416 29.2012 -3.11035 32.9033l-97.71 85.0811c-24.8994 21.6797 -39.1797 52.8926 -39.1797 85.6338v56.9531c0 47.4277 44.8457 82.0215 91.0459 71.1807c1.96094 55.751 63.5107 87.8262 110.671 60.8057
c29.1895 31.0713 78.8604 31.4473 108.334 -0.0214844c32.7051 18.6846 76.4121 10.3096 98.8135 -23.5879zM464 186.594v114.445c0 34.29 -52 33.8232 -52 0.676758c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v26.751
c0 34.457 -52 33.707 -52 0.676758v-27.4287c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v40.4658c0 34.3525 -52 33.8115 -52 0.677734v-41.1436c0 -8.83594 -7.16406 -16 -16 -16h-7c-8.83594 0 -16 7.16406 -16 16v26.751
c0 34.4023 -52 33.7744 -52 0.676758v-116.571c0 -8.83203 -7.16797 -16 -16 -16c-3.30664 0 -8.01367 1.7627 -10.5068 3.93359l-7 6.09473c-3.03223 2.64062 -5.49316 8.04688 -5.49316 12.0674v0v41.2275c0 34.2148 -52 33.8857 -52 0.677734v-56.9531
c0 -18.8555 8.27441 -36.874 22.7002 -49.4365l97.71 -85.0801c12.4502 -10.8398 19.5898 -26.4463 19.5898 -42.8164v-10.2861h220v7.07617c0 13.21 2.65332 26.0791 7.88281 38.25l42.835 99.6553c2.91602 6.75391 5.28223 18.207 5.28223 25.5635v0.0488281z" />
<glyph glyph-name="hand-paper" unicode="&#xf256;" horiz-adv-x="448"
d="M372.57 335.359c39.9062 5.63281 75.4297 -25.7393 75.4297 -66.3594v-131.564c-0.00195312 -12.7666 -2.33008 -33.2246 -5.19531 -45.666l-30.1836 -130.958c-3.34668 -14.5234 -16.2783 -24.8125 -31.1816 -24.8125h-222.897
c-9.10352 0 -20.7793 6.01758 -26.0615 13.4316l-119.97 168.415c-21.2441 29.8203 -14.8047 71.3574 14.5498 93.1533c18.7754 13.9395 42.1309 16.2979 62.083 8.87109v126.13c0 44.0547 41.125 75.5439 82.4053 64.9834c23.8926 48.1963 92.3535 50.2471 117.982 0.74707
c42.5186 11.1445 83.0391 -21.9346 83.0391 -65.5469v-10.8242zM399.997 137.437l-0.00195312 131.563c0 24.9492 -36.5703 25.5508 -36.5703 -0.691406v-76.3086c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v154.184
c0 25.501 -36.5703 26.3633 -36.5703 0.691406v-154.875c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v188.309c0 25.501 -36.5703 26.3545 -36.5703 0.691406v-189c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16
v153.309c0 25.501 -36.5713 26.3359 -36.5713 0.691406v-206.494c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832l-27.1279 38.0791c-14.3711 20.1709 -43.833 -2.33496 -29.3945 -22.6045l115.196 -161.697h201.92l27.3252 118.551
c2.63086 11.417 3.96484 23.1553 3.96484 34.8857z" />
<glyph glyph-name="hand-scissors" unicode="&#xf257;"
d="M256 -32c-44.9561 0 -77.3428 43.2627 -64.0244 85.8535c-21.6484 13.71 -34.0156 38.7617 -30.3408 65.0068h-87.6348c-40.8037 0 -74 32.8105 -74 73.1406c0 40.3291 33.1963 73.1396 74 73.1396l94 -9.14062l-78.8496 18.6787
c-38.3076 14.7422 -57.04 57.4707 -41.9424 95.1123c15.0303 37.4736 57.7549 55.7803 95.6416 41.2012l144.929 -55.7568c24.9551 30.5566 57.8086 43.9932 92.2178 24.7324l97.999 -54.8525c20.9746 -11.7393 34.0049 -33.8457 34.0049 -57.6904v-205.702
c0 -30.7422 -21.4404 -57.5576 -51.7979 -64.5537l-118.999 -27.4268c-4.97168 -1.14648 -10.0889 -1.72949 -15.2031 -1.72949zM256 16.0127l70 -0.000976562c1.23633 0 3.21777 0.225586 4.42285 0.501953l119.001 27.4277
c8.58203 1.97754 14.5762 9.29102 14.5762 17.7812v205.701c0 6.4873 -3.62109 12.542 -9.44922 15.8047l-98 54.8545c-8.13965 4.55566 -18.668 2.61914 -24.4873 -4.50781l-21.7646 -26.6475c-2.65039 -3.24512 -8.20215 -5.87891 -12.3926 -5.87891
c-1.64062 0 -4.21484 0.477539 -5.74609 1.06738l-166.549 64.0908c-32.6543 12.5664 -50.7744 -34.5771 -19.2227 -46.7168l155.357 -59.7852c5.66016 -2.17773 10.2539 -8.86816 10.2539 -14.9326v0v-11.6328c0 -8.83691 -7.16309 -16 -16 -16h-182
c-34.375 0 -34.4297 -50.2803 0 -50.2803h182c8.83691 0 16 -7.16309 16 -16v-6.85645c0 -8.83691 -7.16309 -16 -16 -16h-28c-25.1221 0 -25.1592 -36.5674 0 -36.5674h28c8.83691 0 16 -7.16211 16 -16v-6.85547c0 -8.83691 -7.16309 -16 -16 -16
c-25.1201 0 -25.1602 -36.5674 0 -36.5674z" />
<glyph glyph-name="hand-lizard" unicode="&#xf258;" horiz-adv-x="576"
d="M556.686 157.458c12.6357 -19.4863 19.3145 -42.0615 19.3145 -65.2871v-124.171h-224v71.582l-99.751 38.7871c-2.7832 1.08203 -5.70996 1.63086 -8.69727 1.63086h-131.552c-30.8789 0 -56 25.1211 -56 56c0 48.5234 39.4766 88 88 88h113.709l18.333 48h-196.042
c-44.1123 0 -80 35.8877 -80 80v8c0 30.8779 25.1211 56 56 56h293.917c24.5 0 47.084 -12.2725 60.4111 -32.8291zM528 16v76.1709v0.0478516c0 11.7461 -5.19141 29.2734 -11.5879 39.124l-146.358 225.715c-4.44336 6.85254 -11.9707 10.9424 -20.1367 10.9424h-293.917
c-4.41113 0 -8 -3.58887 -8 -8v-8c0 -17.6445 14.3555 -32 32 -32h213.471c25.2021 0 42.626 -25.293 33.6299 -48.8457l-24.5518 -64.2812c-7.05371 -18.4658 -25.0732 -30.873 -44.8398 -30.873h-113.709c-22.0557 0 -40 -17.9443 -40 -40c0 -4.41113 3.58887 -8 8 -8
h131.552h0.0517578c7.44141 0 19.1074 -2.19238 26.041 -4.89355l99.752 -38.7881c18.5898 -7.22852 30.6035 -24.7881 30.6035 -44.7363v-23.582h128z" />
<glyph glyph-name="hand-spock" unicode="&#xf259;"
d="M21.0957 66.21c-26.9688 25.3818 -28.2471 67.7461 -2.87109 94.707c24.1982 25.7139 64.2881 28.2373 91.4824 5.72168l-31.04 136.509c-9.38379 41.2803 21.4336 81.0127 64.0713 81.8438c1.74414 28.9062 22.2656 54.4912 51.8818 61.2949
c36.001 8.27539 72.0176 -14.2266 80.3037 -50.2959l21.6748 -131.99l16.9014 105.25c9.02344 36.0947 45.4473 57.7021 81.25 48.75c27.3066 -6.82715 45.7061 -29.1357 49.8496 -53.9922c43.2285 0.212891 75.6436 -40.1133 65.5439 -82.5244l-31.7295 -133.41
c-0.938477 -3.94141 -1.41406 -7.99414 -1.41406 -12.0449v-36.8389v-0.00683594c0 -9.29102 -2.14355 -24.0596 -4.78516 -32.9668l-31.8145 -107.312c-4.02734 -13.585 -16.5107 -22.9043 -30.6807 -22.9043h-237.6c-7.00586 0 -16.8311 3.89648 -21.9316 8.69824z
M53.1641 128.021c-7.17969 -7.62891 -6.81543 -19.6777 0.813477 -26.8574l124.487 -117.164h219.311l28.4199 95.8613c1.86133 6.27637 2.80469 12.7793 2.80469 19.3281v36.8389c0.000976562 6.48047 1.21973 16.8574 2.71973 23.1621l31.7549 133.407
c5.83105 24.4893 -31.1445 33.25 -36.9658 8.80273l-26.9229 -113.105c-1.61523 -6.78711 -8.58887 -12.2949 -15.5645 -12.2949h-9.69434c-10.4072 0 -18.043 9.79199 -15.5225 19.8799l38.127 152.512c6.09766 24.376 -30.7607 33.6396 -36.8643 9.21777l-42.3721 -169.49
c-1.67285 -6.68945 -8.62695 -12.1191 -15.5225 -12.1191h-13.2168v0c-7.0332 0 -14.0195 5.5625 -15.5938 12.417l-45.2207 196.828c-5.64453 24.5684 -42.6572 15.9609 -37.0342 -8.50781l41.6191 -181.153c2.30078 -10.0156 -5.31738 -19.583 -15.5938 -19.583h-8.60352
h-0.000976562c-7.0498 0 -14.04 5.5791 -15.6025 12.4541l-30.3984 133.757c-5.55273 24.4395 -42.6504 16.1963 -37.0547 -8.4209l34.1299 -150.172c0.263672 -1.16309 0.397461 -2.35352 0.397461 -3.5459v-69.4795c0 -13.9941 -16.7754 -21.2432 -26.9658 -11.6523
l-53.0117 49.8936c-7.61523 7.16699 -19.6377 6.85938 -26.8564 -0.8125z" />
<glyph glyph-name="hand-pointer" unicode="&#xf25a;" horiz-adv-x="448"
d="M358.182 268.639c43.1934 16.6348 89.8184 -15.7949 89.8184 -62.6387v-84c-0.000976562 -4.25 -0.775391 -11.0615 -1.72754 -15.2041l-27.4297 -118.999c-6.98242 -30.2969 -33.7549 -51.7969 -64.5566 -51.7969h-178.286c-21.2588 0 -41.3682 10.4102 -53.791 27.8457
l-109.699 154.001c-21.2432 29.8193 -14.8047 71.3574 14.5498 93.1523c18.8115 13.9658 42.1748 16.2822 62.083 8.87207v161.129c0 36.9443 29.7363 67 66.2861 67s66.2861 -30.0557 66.2861 -67v-73.6338c20.4131 2.85742 41.4678 -3.94238 56.5947 -19.6289
c27.1934 12.8467 60.3799 5.66992 79.8721 -19.0986zM80.9854 168.303c-14.4004 20.2119 -43.8008 -2.38281 -29.3945 -22.6055l109.712 -154c3.43457 -4.81934 8.92871 -7.69727 14.6973 -7.69727h178.285c8.49219 0 15.8037 5.99414 17.7822 14.5762l27.4297 119.001
c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285v84c0 25.1602 -36.5713 25.1211 -36.5713 0c0 -8.83594 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16406 -16 16v21c0 25.1602 -36.5713 25.1201 -36.5713 0v-21c0 -8.83594 -7.16309 -16 -16 -16h-6.85938
c-8.83691 0 -16 7.16406 -16 16v35c0 25.1602 -36.5703 25.1201 -36.5703 0v-35c0 -8.83594 -7.16309 -16 -16 -16h-6.85742c-8.83691 0 -16 7.16406 -16 16v175c0 25.1602 -36.5713 25.1201 -36.5713 0v-241.493c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832z
M176.143 48v96c0 8.83691 6.26855 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16zM251.571 48v96c0 8.83691 6.26758 16 14 16h6c7.73145 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26855 -16 -14 -16h-6
c-7.73242 0 -14 7.16309 -14 16zM327 48v96c0 8.83691 6.26758 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16z" />
<glyph glyph-name="hand-peace" unicode="&#xf25b;" horiz-adv-x="448"
d="M362.146 256.024c42.5908 13.3184 85.8535 -19.0684 85.8535 -64.0244l-0.0117188 -70.001c-0.000976562 -4.25 -0.775391 -11.0615 -1.72949 -15.2031l-27.4268 -118.999c-6.99707 -30.3564 -33.8105 -51.7969 -64.5547 -51.7969h-205.702
c-23.8447 0 -45.9502 13.0303 -57.6904 34.0059l-54.8525 97.999c-19.2607 34.4092 -5.82422 67.2617 24.7324 92.2178l-55.7568 144.928c-14.5791 37.8867 3.72754 80.6113 41.2012 95.6416c37.6406 15.0977 80.3691 -3.63477 95.1123 -41.9424l18.6787 -78.8496
l-9.14062 94c0 40.8037 32.8096 74 73.1396 74s73.1406 -33.1963 73.1406 -74v-87.6348c26.2451 3.6748 51.2959 -8.69238 65.0068 -30.3408zM399.987 122l-0.000976562 70c0 25.1602 -36.5674 25.1201 -36.5674 0c0 -8.83691 -7.16309 -16 -16 -16h-6.85547
c-8.83789 0 -16 7.16309 -16 16v28c0 25.1592 -36.5674 25.1221 -36.5674 0v-28c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v182c0 34.4297 -50.2803 34.375 -50.2803 0v-182c0 -8.83691 -7.16309 -16 -16 -16h-11.6328v0
c-6.06445 0 -12.7549 4.59375 -14.9326 10.2539l-59.7842 155.357c-12.1396 31.5518 -59.2842 13.4326 -46.7168 -19.2227l64.0898 -166.549c0.589844 -1.53125 1.06738 -4.10547 1.06738 -5.74609c0 -4.19043 -2.63379 -9.74219 -5.87891 -12.3926l-26.6475 -21.7646
c-7.12695 -5.81934 -9.06445 -16.3467 -4.50781 -24.4873l54.8535 -98c3.26367 -5.82812 9.31934 -9.44922 15.8057 -9.44922h205.701c8.49121 0 15.8037 5.99414 17.7812 14.5762l27.4277 119.001c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285z" />
<glyph glyph-name="registered" unicode="&#xf25d;"
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
M366.442 73.791c4.40332 -7.99219 -1.37012 -17.791 -10.5107 -17.791h-42.8096h-0.0126953c-3.97559 0 -8.71582 2.84961 -10.5801 6.36035l-47.5156 89.3027h-31.958v-83.6631c0 -6.61719 -5.38281 -12 -12 -12h-38.5674c-6.61719 0 -12 5.38281 -12 12v248.304
c0 6.61719 5.38281 12 12 12h78.667c71.251 0 101.498 -32.749 101.498 -85.252c0 -31.6123 -15.2148 -59.2969 -39.4824 -73.1758c3.02148 -4.61719 0.225586 0.199219 53.2715 -96.085zM256.933 208.094c20.9131 0 32.4307 11.5186 32.4316 32.4316
c0 19.5752 -6.5127 31.709 -38.9297 31.709h-27.377v-64.1406h33.875z" />
<glyph glyph-name="calendar-plus" unicode="&#xf271;" horiz-adv-x="448"
d="M336 156v-24c0 -6.59961 -5.40039 -12 -12 -12h-76v-76c0 -6.59961 -5.40039 -12 -12 -12h-24c-6.59961 0 -12 5.40039 -12 12v76h-76c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h76v76c0 6.59961 5.40039 12 12 12h24c6.59961 0 12 -5.40039 12 -12
v-76h76c6.59961 0 12 -5.40039 12 -12zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40
c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-minus" unicode="&#xf272;" horiz-adv-x="448"
d="M124 120c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h200c6.59961 0 12 -5.40039 12 -12v-24c0 -6.59961 -5.40039 -12 -12 -12h-200zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-times" unicode="&#xf273;" horiz-adv-x="448"
d="M311.7 73.2998l-17 -17c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-53.7002 53.7998l-53.7002 -53.6992c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-17 17c-4.7002 4.69922 -4.7002 12.2998 0 17l53.7002 53.6992l-53.7002 53.7002c-4.7002 4.7002 -4.7002 12.2998 0 17
l17 17c4.7002 4.7002 12.2998 4.7002 17 0l53.7002 -53.7002l53.7002 53.7002c4.7002 4.7002 12.2998 4.7002 17 0l17 -17c4.7002 -4.7002 4.7002 -12.2998 0 -17l-53.7998 -53.7998l53.6992 -53.7002c4.80078 -4.7002 4.80078 -12.2998 0.100586 -17zM448 336v-352
c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10
v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="calendar-check" unicode="&#xf274;" horiz-adv-x="448"
d="M400 384c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h48v52c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-52h128v52c0 6.62695 5.37305 12 12 12h40
c6.62695 0 12 -5.37305 12 -12v-52h48zM394 -16c3.31152 0 6 2.68848 6 6v298h-352v-298c0 -3.31152 2.68848 -6 6 -6h340zM341.151 184.65l-142.31 -141.169c-4.70508 -4.66699 -12.3027 -4.6377 -16.9707 0.0673828l-75.0908 75.6992
c-4.66699 4.70508 -4.6377 12.3027 0.0673828 16.9707l22.7197 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l44.1035 -44.4609l111.072 110.182c4.70508 4.66699 12.3027 4.63672 16.9707 -0.0683594l22.5361 -22.7178
c4.66699 -4.70508 4.63672 -12.3027 -0.0683594 -16.9697z" />
<glyph glyph-name="map" unicode="&#xf279;" horiz-adv-x="576"
d="M560.02 416c8.4502 0 15.9805 -6.83008 15.9805 -16.0195v-346.32c0 -11.9609 -9.01367 -25.2705 -20.1201 -29.71l-151.83 -52.8105c-5.32617 -1.7334 -14.1953 -3.13965 -19.7969 -3.13965c-5.7373 0 -14.8105 1.47363 -20.2529 3.29004l-172 60.71l-170.05 -62.8398
c-1.99023 -0.790039 -4 -1.16016 -5.95996 -1.16016c-8.45996 0 -15.9902 6.83008 -15.9902 16.0195v346.32c0.00292969 11.959 9.0166 25.2686 20.1201 29.71l151.83 52.8105c6.43945 2.08984 13.1201 3.13965 19.8096 3.13965
c5.73242 -0.00195312 14.8008 -1.47168 20.2402 -3.28027l172 -60.7197h0.00976562l170.05 62.8398c1.98047 0.790039 4 1.16016 5.95996 1.16016zM224 357.58v-285.97l128 -45.1904v285.97zM48 29.9502l127.36 47.0801l0.639648 0.229492v286.2l-128 -44.5303v-288.979z
M528 65.0801v288.97l-127.36 -47.0693l-0.639648 -0.240234v-286.19z" />
<glyph glyph-name="comment-alt" unicode="&#xf27a;"
d="M448 448c35.2998 0 64 -28.7002 64 -64v-288c0 -35.2998 -28.7002 -64 -64 -64h-144l-124.9 -93.5996c-2.19922 -1.7002 -4.69922 -2.40039 -7.09961 -2.40039c-6.2002 0 -12 4.90039 -12 12v84h-96c-35.2998 0 -64 28.7002 -64 64v288c0 35.2998 28.7002 64 64 64h384z
M464 96v288c0 8.7998 -7.2002 16 -16 16h-384c-8.7998 0 -16 -7.2002 -16 -16v-288c0 -8.7998 7.2002 -16 16 -16h144v-60l67.2002 50.4004l12.7998 9.59961h160c8.7998 0 16 7.2002 16 16z" />
<glyph glyph-name="pause-circle" unicode="&#xf28b;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16zM240 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16z" />
<glyph glyph-name="stop-circle" unicode="&#xf28d;"
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-160
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h160c8.7998 0 16 -7.2002 16 -16z" />
<glyph glyph-name="handshake" unicode="&#xf2b5;" horiz-adv-x="640"
d="M519.2 320.1h120.8v-255.699h-64c-17.5 0 -31.7998 14.1992 -31.9004 31.6992h-57.8994c-1.7998 -8.19922 -5.2998 -16.0996 -10.9004 -23l-26.2002 -32.2998c-15.7998 -19.3994 -41.8994 -25.5 -64 -16.7998c-13.5 -16.5996 -30.5996 -24 -48.7998 -24
c-15.0996 0 -28.5996 5.09961 -41.0996 15.9004c-31.7998 -21.9004 -74.7002 -21.3008 -105.601 3.7998l-84.5996 76.3994h-9.09961c-0.100586 -17.5 -14.3008 -31.6992 -31.9004 -31.6992h-64v255.699h118l47.5996 47.6006c10.5 10.3994 24.8008 16.2998 39.6006 16.2998
h226.8v0c12.7812 0 30.5225 -7.30273 39.5996 -16.2998zM48 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16zM438 103.3c2.7002 3.40039 2.2002 8.5 -1.2002 11.2998l-108.2 87.8008l-8.19922 -7.5
c-40.3008 -36.8008 -86.7002 -11.8008 -101.5 4.39941c-26.7002 29 -25 74.4004 4.39941 101.3l38.7002 35.5h-56.7002c-2 -0.799805 -3.7002 -1.5 -5.7002 -2.2998l-61.6992 -61.5996h-41.9004v-128.101h27.7002l97.2998 -88
c16.0996 -13.0996 41.4004 -10.5 55.2998 6.60059l15.6006 19.2002l36.7998 -31.5c3 -2.40039 12 -4.90039 18 2.39941l30 36.5l23.8994 -19.3994c3.5 -2.80078 8.5 -2.2002 11.3008 1.19922zM544 144.1v128h-44.7002l-61.7002 61.6006
c-1.39941 1.5 -3.39941 2.2998 -5.5 2.2998l-83.6992 -0.200195c-10 0 -19.6006 -3.7002 -27 -10.5l-65.6006 -60.0996c-9.7002 -8.7998 -10.5 -24 -1.2002 -33.9004c8.90039 -9.39941 25.1006 -8.7002 34.6006 0l55.2002 50.6006c6.5 5.89941 16.5996 5.5 22.5996 -1
l10.9004 -11.7002c6 -6.5 5.5 -16.6006 -1 -22.6006l-12.5 -11.3994l102.699 -83.4004c2.80078 -2.2998 5.40039 -4.89941 7.7002 -7.7002h69.2002zM592 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16z
" />
<glyph glyph-name="envelope-open" unicode="&#xf2b6;"
d="M494.586 283.484c9.6123 -7.94824 17.4141 -24.5205 17.4141 -36.9932v-262.491c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v262.515c0 12.5166 7.84668 29.1279 17.5146 37.0771c4.08008 3.35449 110.688 89.0996 135.15 108.549
c22.6992 18.1426 60.1299 55.8594 103.335 55.8594c43.4365 0 81.2314 -38.1914 103.335 -55.8594c23.5283 -18.707 130.554 -104.773 135.251 -108.656zM464 -10v253.632v0.00488281c0 1.5791 -0.996094 3.66602 -2.22363 4.6582
c-15.8633 12.8232 -108.793 87.5752 -132.366 106.316c-17.5527 14.0195 -49.7168 45.3887 -73.4102 45.3887c-23.6016 0 -55.2451 -30.8799 -73.4102 -45.3887c-23.5713 -18.7393 -116.494 -93.4795 -132.364 -106.293
c-1.40918 -1.13965 -2.22559 -2.85254 -2.22559 -4.66504v-253.653c0 -3.31152 2.68848 -6 6 -6h404c3.31152 0 6 2.68848 6 6zM432.009 177.704c4.24902 -5.15918 3.46484 -12.7949 -1.74512 -16.9814c-28.9746 -23.2822 -59.2734 -47.5967 -70.9287 -56.8623
c-22.6992 -18.1436 -60.1299 -55.8604 -103.335 -55.8604c-43.4521 0 -81.2871 38.2373 -103.335 55.8604c-11.2793 8.9668 -41.7441 33.4131 -70.9268 56.8643c-5.20996 4.1875 -5.99316 11.8223 -1.74512 16.9814l15.2578 18.5283
c4.17773 5.07227 11.6572 5.84277 16.7793 1.72559c28.6182 -23.001 58.5654 -47.0352 70.5596 -56.5713c17.5527 -14.0195 49.7168 -45.3887 73.4102 -45.3887c23.6016 0 55.2461 30.8799 73.4102 45.3887c11.9941 9.53516 41.9434 33.5703 70.5625 56.5684
c5.12207 4.11621 12.6016 3.3457 16.7783 -1.72656z" />
<glyph glyph-name="address-book" unicode="&#xf2b9;" horiz-adv-x="448"
d="M436 288h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48
h320c26.5 0 48 -21.5 48 -48v-48h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM368 -16v416h-320v-416h320zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM118.4 64
c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002
h-179.199z" />
<glyph glyph-name="address-card" unicode="&#xf2bb;" horiz-adv-x="576"
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v352h-480v-352h480zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z
M118.4 64c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002
c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199zM360 128c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 192c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 256c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112z" />
<glyph glyph-name="user-circle" unicode="&#xf2bd;" horiz-adv-x="496"
d="M248 344c53 0 96 -43 96 -96s-43 -96 -96 -96s-96 43 -96 96s43 96 96 96zM248 200c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
c49.7002 0 95.0996 18.2998 130.1 48.4004c-14.8994 23 -40.3994 38.5 -69.5996 39.5c-20.7998 -6.5 -40.5996 -9.60059 -60.5 -9.60059s-39.7002 3.2002 -60.5 9.60059c-29.2002 -0.900391 -54.7002 -16.5 -69.5996 -39.5c35 -30.1006 80.3994 -48.4004 130.1 -48.4004z
M410.7 76.0996c23.3994 32.7002 37.2998 72.7002 37.2998 115.9c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -43.2002 13.9004 -83.2002 37.2998 -115.9c24.5 31.4004 62.2002 51.9004 105.101 51.9004c10.1992 0 26.0996 -9.59961 57.5996 -9.59961
c31.5996 0 47.4004 9.59961 57.5996 9.59961c43 0 80.7002 -20.5 105.101 -51.9004z" />
<glyph glyph-name="id-badge" unicode="&#xf2c1;" horiz-adv-x="384"
d="M336 448c26.5 0 48 -21.5 48 -48v-416c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h288zM336 -16v416h-288v-416h288zM144 336c-8.7998 0 -16 7.2002 -16 16s7.2002 16 16 16h96c8.7998 0 16 -7.2002 16 -16s-7.2002 -16 -16 -16
h-96zM192 160c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM102.4 32c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8
c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199z" />
<glyph glyph-name="id-card" unicode="&#xf2c2;" horiz-adv-x="576"
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v288h-480v-288h32.7998c-1 4.5 -0.799805 -3.59961 -0.799805 22.4004c0 31.7998 30.0996 57.5996 67.2002 57.5996
c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996c0 -26 0.0996094 -17.9004 -0.799805 -22.4004h224.8zM360 96c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16
c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 160c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 224c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM192 128c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z" />
<glyph glyph-name="window-maximize" unicode="&#xf2d0;"
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v234h-416v-234c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="window-minimize" unicode="&#xf2d1;"
d="M480 -32h-448c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32h448c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
<glyph glyph-name="window-restore" unicode="&#xf2d2;"
d="M464 448c26.5 0 48 -21.5 48 -48v-320c0 -26.5 -21.5 -48 -48 -48h-48v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v320c0 26.5 21.5 48 48 48h48v48c0 26.5 21.5 48 48 48h320zM368 -16v208h-320v-208h320zM464 80v320h-320v-48h224
c26.5 0 48 -21.5 48 -48v-224h48z" />
<glyph glyph-name="snowflake" unicode="&#xf2dc;" horiz-adv-x="448"
d="M440.1 92.7998c7.60059 -4.39941 10.1006 -14.2002 5.5 -21.7002l-7.89941 -13.8994c-4.40039 -7.7002 -14 -10.2998 -21.5 -5.90039l-39.2002 23l9.09961 -34.7002c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.5996l-15.2002 -4.09961
c-8.39941 -2.30078 -17.0996 2.7998 -19.2998 11.2998l-21.2998 81l-71.9004 42.2002v-84.5l58.2998 -59.3008c6.10059 -6.19922 6.10059 -16.3994 0 -22.5996l-11.0996 -11.2998c-6.09961 -6.2002 -16.0996 -6.2002 -22.2002 0l-24.8994 25.3994v-46.0996
c0 -8.7998 -7 -16 -15.7002 -16h-15.7002c-8.7002 0 -15.7002 7.2002 -15.7002 16v45.9004l-24.8994 -25.4004c-6.10059 -6.2002 -16.1006 -6.2002 -22.2002 0l-11.1006 11.2998c-6.09961 6.2002 -6.09961 16.4004 0 22.6006l58.3008 59.2998v84.5l-71.9004 -42.2002
l-21.2998 -81c-2.2998 -8.5 -10.9004 -13.5996 -19.2998 -11.2998l-15.2002 4.09961c-8.40039 2.2998 -13.2998 11.1006 -11.1006 19.6006l9.10059 34.6992l-39.2002 -23c-7.5 -4.39941 -17.2002 -1.7998 -21.5 5.90039l-7.90039 13.9004
c-4.2998 7.69922 -1.69922 17.5 5.80078 21.8994l39.1992 23l-34.0996 9.2998c-8.40039 2.30078 -13.2998 11.1006 -11.0996 19.6006l4.09961 15.5c2.2998 8.5 10.9004 13.5996 19.2998 11.2998l79.7002 -21.7002l71.9004 42.2002l-71.9004 42.2002l-79.7002 -21.7002
c-8.39941 -2.2998 -17.0996 2.7998 -19.2998 11.2998l-4.09961 15.5c-2.30078 8.5 2.69922 17.2998 11.0996 19.6006l34.0996 9.09961l-39.1992 23c-7.60059 4.5 -10.1006 14.2002 -5.80078 21.9004l7.90039 13.8994c4.40039 7.7002 14 10.2998 21.5 5.90039l39.2002 -23
l-9.10059 34.7002c-2.2998 8.5 2.7002 17.2998 11.1006 19.5996l15.2002 4.09961c8.39941 2.30078 17.0996 -2.7998 19.2998 -11.2998l21.2998 -81l71.9004 -42.2002v84.5l-58.3008 59.3008c-6.09961 6.19922 -6.09961 16.3994 0 22.5996l11.5 11.2998
c6.10059 6.2002 16.1006 6.2002 22.2002 0l24.9004 -25.3994v46.0996c0 8.7998 7 16 15.7002 16h15.6992c8.7002 0 15.7002 -7.2002 15.7002 -16v-45.9004l24.9004 25.4004c6.09961 6.2002 16.0996 6.2002 22.2002 0l11.0996 -11.2998
c6.09961 -6.2002 6.09961 -16.4004 0 -22.6006l-58.2998 -59.2998v-84.5l71.8994 42.2002l21.3008 81c2.2998 8.5 10.8994 13.5996 19.2998 11.2998l15.2002 -4.09961c8.39941 -2.2998 13.2998 -11.1006 11.0996 -19.6006l-9.09961 -34.6992l39.1992 23
c7.5 4.39941 17.2002 1.7998 21.5 -5.90039l7.90039 -13.9004c4.2998 -7.69922 1.7002 -17.5 -5.7998 -21.8994l-39.2002 -23l34.0996 -9.2998c8.40039 -2.30078 13.3008 -11.1006 11.1006 -19.6006l-4.10059 -15.5c-2.2998 -8.5 -10.8994 -13.5996 -19.2998 -11.2998
l-79.7002 21.7002l-71.8994 -42.2002l71.7998 -42.2002l79.7002 21.7002c8.39941 2.2998 17.0996 -2.7998 19.2998 -11.2998l4.09961 -15.5c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.6006l-34.0996 -9.2998z" />
<glyph glyph-name="trash-alt" unicode="&#xf2ed;" horiz-adv-x="448"
d="M268 32c-6.62402 0 -12 5.37598 -12 12v216c0 6.62402 5.37598 12 12 12h24c6.62402 0 12 -5.37598 12 -12v-216c0 -6.62402 -5.37598 -12 -12 -12h-24zM432 368c8.83203 0 16 -7.16797 16 -16v-16c0 -8.83203 -7.16797 -16 -16 -16h-16v-336
c0 -26.4961 -21.5039 -48 -48 -48h-288c-26.4961 0 -48 21.5039 -48 48v336h-16c-8.83203 0 -16 7.16797 -16 16v16c0 8.83203 7.16797 16 16 16h82.4102l34.0195 56.7002c7.71875 12.8613 26.1572 23.2998 41.1572 23.2998h0.00292969h100.82h0.0224609
c15 0 33.4385 -10.4385 41.1572 -23.2998l34 -56.7002h82.4102zM171.84 397.09l-17.4502 -29.0898h139.221l-17.46 29.0898c-0.96582 1.60645 -3.26953 2.91016 -5.14355 2.91016h-0.00683594h-94h-0.0166016c-1.87402 0 -4.17871 -1.30371 -5.14355 -2.91016zM368 -16v336
h-288v-336h288zM156 32c-6.62402 0 -12 5.37598 -12 12v216c0 6.62402 5.37598 12 12 12h24c6.62402 0 12 -5.37598 12 -12v-216c0 -6.62402 -5.37598 -12 -12 -12h-24z" />
<glyph glyph-name="images" unicode="&#xf302;" horiz-adv-x="576"
d="M480 32v-16c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256c0 26.5098 21.4902 48 48 48h16v-48h-10c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v10h48zM522 368h-372
c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v244c0 3.31152 -2.68848 6 -6 6zM528 416c26.5098 0 48 -21.4902 48 -48v-256c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256
c0 26.5098 21.4902 48 48 48h384zM264 304c0 -22.0908 -17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40s40 -17.9092 40 -40zM192 208l39.5146 39.5146c4.68652 4.68652 12.2842 4.68652 16.9717 0l39.5137 -39.5146l103.515 103.515
c4.68652 4.68652 12.2842 4.68652 16.9717 0l71.5137 -71.5146v-80h-288v48z" />
<glyph glyph-name="clipboard" unicode="&#xf328;" horiz-adv-x="384"
d="M336 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h80c0 35.2998 28.7002 64 64 64s64 -28.7002 64 -64h80zM192 408c-13.2998 0 -24 -10.7002 -24 -24s10.7002 -24 24 -24s24 10.7002 24 24
s-10.7002 24 -24 24zM336 -10v340c0 3.2998 -2.7002 6 -6 6h-42v-36c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v36h-42c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h276c3.2998 0 6 2.7002 6 6z" />
<glyph glyph-name="arrow-alt-circle-down" unicode="&#xf358;"
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM224 308c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-116
h67c10.7002 0 16.0996 -12.9004 8.5 -20.5l-99 -99c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-99 99c-7.5 7.59961 -2.2002 20.5 8.5 20.5h67v116z" />
<glyph glyph-name="arrow-alt-circle-left" unicode="&#xf359;"
d="M8 192c0 137 111 248 248 248s248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248zM456 192c0 110.5 -89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200zM384 212v-40c0 -6.59961 -5.40039 -12 -12 -12h-116v-67
c0 -10.7002 -12.9004 -16 -20.5 -8.5l-99 99c-4.7002 4.7002 -4.7002 12.2998 0 17l99 99c7.59961 7.59961 20.5 2.2002 20.5 -8.5v-67h116c6.59961 0 12 -5.40039 12 -12z" />
<glyph glyph-name="arrow-alt-circle-right" unicode="&#xf35a;"
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM128 172v40c0 6.59961 5.40039 12 12 12h116v67
c0 10.7002 12.9004 16 20.5 8.5l99 -99c4.7002 -4.7002 4.7002 -12.2998 0 -17l-99 -99c-7.59961 -7.59961 -20.5 -2.2002 -20.5 8.5v67h-116c-6.59961 0 -12 5.40039 -12 12z" />
<glyph glyph-name="arrow-alt-circle-up" unicode="&#xf35b;"
d="M256 -56c-137 0 -248 111 -248 248s111 248 248 248s248 -111 248 -248s-111 -248 -248 -248zM256 392c-110.5 0 -200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200zM276 64h-40c-6.59961 0 -12 5.40039 -12 12v116h-67
c-10.7002 0 -16 12.9004 -8.5 20.5l99 99c4.7002 4.7002 12.2998 4.7002 17 0l99 -99c7.59961 -7.59961 2.2002 -20.5 -8.5 -20.5h-67v-116c0 -6.59961 -5.40039 -12 -12 -12z" />
<glyph glyph-name="gem" unicode="&#xf3a5;" horiz-adv-x="576"
d="M464 448c4.09961 0 7.7998 -2 10.0996 -5.40039l99.9004 -147.199c2.90039 -4.40039 2.59961 -10.1006 -0.700195 -14.2002l-276 -340.8c-4.7998 -5.90039 -13.7998 -5.90039 -18.5996 0l-276 340.8c-3.2998 4 -3.60059 9.7998 -0.700195 14.2002l100 147.199
c2.2002 3.40039 6 5.40039 10 5.40039h352zM444.7 400h-56.7998l51.6992 -96h68.4004zM242.6 400l-51.5996 -96h194l-51.7002 96h-90.7002zM131.3 400l-63.2998 -96h68.4004l51.6992 96h-56.7998zM88.2998 256l119.7 -160l-68.2998 160h-51.4004zM191.2 256l96.7998 -243.3
l96.7998 243.3h-193.6zM368 96l119.6 160h-51.3994z" />
<glyph glyph-name="money-bill-alt" unicode="&#xf3d1;" horiz-adv-x="640"
d="M320 304c53.0195 0 96 -50.1396 96 -112c0 -61.8701 -43 -112 -96 -112c-53.0195 0 -96 50.1504 -96 112c0 61.8604 42.9805 112 96 112zM360 136v16c0 4.41992 -3.58008 8 -8 8h-16v88c0 4.41992 -3.58008 8 -8 8h-13.5801h-0.000976562
c-4.01074 0 -9.97266 -1.80566 -13.3086 -4.03027l-15.3301 -10.2197c-1.96777 -1.30957 -3.56445 -4.29004 -3.56445 -6.65332c0 -1.33691 0.601562 -3.32422 1.34375 -4.43652l8.88086 -13.3105c1.30859 -1.9668 4.29004 -3.56445 6.65332 -3.56445
c1.33691 0 3.32422 0.602539 4.43652 1.34473l0.469727 0.310547v-55.4404h-16c-4.41992 0 -8 -3.58008 -8 -8v-16c0 -4.41992 3.58008 -8 8 -8h64c4.41992 0 8 3.58008 8 8zM608 384c17.6699 0 32 -14.3301 32 -32v-320c0 -17.6699 -14.3301 -32 -32 -32h-576
c-17.6699 0 -32 14.3301 -32 32v320c0 17.6699 14.3301 32 32 32h576zM592 112v160c-35.3496 0 -64 28.6504 -64 64h-416c0 -35.3496 -28.6504 -64 -64 -64v-160c35.3496 0 64 -28.6504 64 -64h416c0 35.3496 28.6504 64 64 64z" />
<glyph glyph-name="window-close" unicode="&#xf410;"
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v340c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z
M356.5 253.4l-61.4004 -61.4004l61.4004 -61.4004c4.59961 -4.59961 4.59961 -12.0996 0 -16.7998l-22.2998 -22.2998c-4.60059 -4.59961 -12.1006 -4.59961 -16.7998 0l-61.4004 61.4004l-61.4004 -61.4004c-4.59961 -4.59961 -12.0996 -4.59961 -16.7998 0
l-22.2998 22.2998c-4.59961 4.60059 -4.59961 12.1006 0 16.7998l61.4004 61.4004l-61.4004 61.4004c-4.59961 4.59961 -4.59961 12.0996 0 16.7998l22.2998 22.2998c4.60059 4.59961 12.1006 4.59961 16.7998 0l61.4004 -61.4004l61.4004 61.4004
c4.59961 4.59961 12.0996 4.59961 16.7998 0l22.2998 -22.2998c4.7002 -4.60059 4.7002 -12.1006 0 -16.7998z" />
<glyph glyph-name="comment-dots" unicode="&#xf4ad;"
d="M144 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM256 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM368 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32s14.2998 32 32 32zM256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5
c-3.80078 8.7998 -2 19 4.59961 26c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160
c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
<glyph glyph-name="smile-wink" unicode="&#xf4da;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM365.8 138.4c10.2002 -8.5 11.6006 -23.6006 3.10059 -33.8008
c-30 -36 -74.1006 -56.5996 -120.9 -56.5996s-90.9004 20.5996 -120.9 56.5996c-8.39941 10.2002 -7.09961 25.3008 3.10059 33.8008c10.0996 8.39941 25.2998 7.09961 33.7998 -3.10059c20.7998 -25.0996 51.5 -39.3994 84 -39.3994s63.2002 14.3994 84 39.3994
c8.5 10.2002 23.5996 11.6006 33.7998 3.10059zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 268c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008l-9.5 8.5
c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996z" />
<glyph glyph-name="angry" unicode="&#xf556;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 136c33.5996 0 65.2002 -14.7998 86.7998 -40.5996
c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008c-10.6006 -8.89941 -25.7002 -6.69922 -33.7998 3c-24.8008 29.7002 -75 29.7002 -99.8008 0c-8.5 -10.1992 -23.5996 -11.5 -33.7998 -3s-11.5996 23.6006 -3.09961 33.8008
c21.5996 25.7998 53.2002 40.5996 86.7998 40.5996zM200 208c0 -17.7002 -14.2998 -32.0996 -32 -32.0996s-32 14.2998 -32 32c0 6.19922 2.2002 11.6992 5.2998 16.5996l-28.2002 8.5c-12.6992 3.7998 -19.8994 17.2002 -16.0996 29.9004
c3.7998 12.6992 17.0996 20 29.9004 16.0996l80 -24c12.6992 -3.7998 19.8994 -17.2002 16.0996 -29.9004c-3.09961 -10.3994 -12.7002 -17.0996 -23 -17.0996zM399 262.9c3.7998 -12.7002 -3.40039 -26.1006 -16.0996 -29.8008l-28.2002 -8.5
c3.09961 -4.89941 5.2998 -10.3994 5.2998 -16.5996c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32c-10.2998 0 -19.9004 6.7002 -23 17.0996c-3.7998 12.7002 3.40039 26.1006 16.0996 29.9004l80 24c12.8008 3.7998 26.1006 -3.40039 29.9004 -16.0996z" />
<glyph glyph-name="dizzy" unicode="&#xf567;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM214.2 209.9
c-7.90039 -7.90039 -20.5 -7.90039 -28.4004 -0.200195l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998
c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.9004c7.7998 7.7998 20.5 7.7998 28.2998 0s7.7998 -20.5 0 -28.2998l-17.9004 -17.9004l17.9004 -17.7998c7.7998 -7.7998 7.7998 -20.5 0 -28.2998zM374.2 302.1
c7.7002 -7.7998 7.7002 -20.3994 0 -28.1992l-17.9004 -17.9004l17.7998 -18c7.80078 -7.7998 7.80078 -20.5 0 -28.2998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0
c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.7998c7.7998 7.80078 20.5 7.80078 28.2998 0zM248 176
c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64s-64 28.7002 -64 64s28.7002 64 64 64z" />
<glyph glyph-name="flushed" unicode="&#xf579;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM344 304c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80
s-80 35.7998 -80 80s35.7998 80 80 80zM344 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM344 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM232 224c0 -44.2002 -35.7998 -80 -80 -80
s-80 35.7998 -80 80s35.7998 80 80 80s80 -35.7998 80 -80zM152 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM152 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM312 104
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
<glyph glyph-name="frown-open" unicode="&#xf57a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32
s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 160c35.5996 0 88.7998 -21.2998 95.7998 -61.2002c2 -11.7998 -9.09961 -21.5996 -20.5 -18.0996
c-31.2002 9.59961 -59.3994 15.2998 -75.2998 15.2998s-44.0996 -5.7002 -75.2998 -15.2998c-11.5 -3.40039 -22.5 6.2998 -20.5 18.0996c7 39.9004 60.2002 61.2002 95.7998 61.2002z" />
<glyph glyph-name="grimace" unicode="&#xf57f;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM344 192c26.5 0 48 -21.5 48 -48v-32c0 -26.5 -21.5 -48 -48 -48h-192c-26.5 0 -48 21.5 -48 48v32c0 26.5 21.5 48 48 48
h192zM176 96v24h-40v-8c0 -8.7998 7.2002 -16 16 -16h24zM176 136v24h-24c-8.7998 0 -16 -7.2002 -16 -16v-8h40zM240 96v24h-48v-24h48zM240 136v24h-48v-24h48zM304 96v24h-48v-24h48zM304 136v24h-48v-24h48zM360 112v8h-40v-24h24c8.7998 0 16 7.2002 16 16zM360 136v8
c0 8.7998 -7.2002 16 -16 16h-24v-24h40z" />
<glyph glyph-name="grin" unicode="&#xf580;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
<glyph glyph-name="grin-alt" unicode="&#xf581;" horiz-adv-x="496"
d="M200.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56
c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM328.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0
c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200
s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006
s79.7002 4.7998 105.6 13.1006z" />
<glyph glyph-name="grin-beam" unicode="&#xf582;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM117.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996
l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002zM277.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998
c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002z" />
<glyph glyph-name="grin-beam-sweat" unicode="&#xf583;" horiz-adv-x="496"
d="M440 288c-29.5 0 -53.2998 26.2998 -53.2998 58.7002c0 25 31.7002 75.5 46.2002 97.2998c3.5 5.2998 10.5996 5.2998 14.1992 0c14.5 -21.7998 46.2002 -72.2998 46.2002 -97.2998c0 -32.4004 -23.7998 -58.7002 -53.2998 -58.7002zM248 48
c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM378.3 216.3
c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM483.6 269.2c8 -24.2998 12.4004 -50.2002 12.4004 -77.2002c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248
c45.7002 0 88.4004 -12.5996 125.2 -34.2002c-10.9004 -21.5996 -15.5 -36.2002 -17.2002 -45.7002c-31.2002 20.1006 -68.2002 31.9004 -108 31.9004c-110.3 0 -200 -89.7002 -200 -200s89.7002 -200 200 -200s200 89.7002 200 200
c0 22.5 -3.90039 44.0996 -10.7998 64.2998c0.399414 0 21.7998 -2.7998 46.3994 12.9004zM168 258.6c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
<glyph glyph-name="grin-hearts" unicode="&#xf584;" horiz-adv-x="496"
d="M353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM200.8 192.3
l-70.2002 18.1006c-20.3994 5.2998 -31.8994 27 -24.1992 47.1992c6.69922 17.7002 26.6992 26.7002 44.8994 22l7.10059 -1.89941l2 7.09961c5.09961 18.1006 22.8994 30.9004 41.5 27.9004c21.3994 -3.40039 34.3994 -24.2002 28.7998 -44.5l-19.4004 -69.9004
c-1.2998 -4.5 -6 -7.2002 -10.5 -6zM389.6 257.6c7.7002 -20.1992 -3.7998 -41.7998 -24.1992 -47.0996l-70.2002 -18.2002c-4.60059 -1.2002 -9.2998 1.5 -10.5 6l-19.4004 69.9004c-5.59961 20.2998 7.40039 41.0996 28.7998 44.5c18.7002 3 36.5 -9.7998 41.5 -27.9004
l2 -7.09961l7.10059 1.89941c18.2002 4.7002 38.2002 -4.39941 44.8994 -22zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200
s89.7002 -200 200 -200z" />
<glyph glyph-name="grin-squint" unicode="&#xf585;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM118.9 184.2c-3.80078 4.39941 -3.90039 11 -0.100586 15.5l33.6006 40.2998
l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5c3.89941 4.40039 10.1992 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5zM361.8 181.7
l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5.10059 2.90039 11.5 1.90039 15.2998 -2.5c3.80078 -4.5 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5
c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5z" />
<glyph glyph-name="grin-squint-tears" unicode="&#xf586;"
d="M117.1 63.9004c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998
c16.8994 16.9004 75.0996 26.9004 100.899 30.6006zM75.9004 105.6c-19.6006 -3.89941 -35.1006 -8.09961 -47.3008 -12.1992c-39.2998 90.5996 -22.0996 199.899 52 274c48.5 48.3994 111.9 72.5996 175.4 72.5996c38.9004 0 77.7998 -9.2002 113.2 -27.4004
c-4 -12.1992 -8.2002 -28 -12 -48.2998c-30.4004 17.9004 -65 27.7002 -101.2 27.7002c-53.4004 0 -103.6 -20.7998 -141.4 -58.5996c-61.5996 -61.5 -74.2998 -153.4 -38.6992 -227.801zM428.2 293.2c20.2998 3.89941 36.2002 8 48.5 12
c47.8994 -93.2002 32.8994 -210.5 -45.2002 -288.601c-48.5 -48.3994 -111.9 -72.5996 -175.4 -72.5996c-33.6992 0 -67.2998 7 -98.6992 20.5996c4.19922 12.2002 8.2998 27.7002 12.1992 47.2002c26.6006 -12.7998 55.9004 -19.7998 86.4004 -19.7998
c53.4004 0 103.6 20.7998 141.4 58.5996c65.6992 65.7002 75.7998 166 30.7998 242.601zM394.9 320.1c-6.30078 -0.899414 -11.7002 4.5 -10.9004 10.9004c3.7002 25.7998 13.7002 84 30.5996 100.9c22 21.8994 57.9004 21.5 80.3008 -0.900391
c22.3994 -22.4004 22.7998 -58.4004 0.899414 -80.2998c-16.8994 -16.9004 -75.0996 -26.9004 -100.899 -30.6006zM207.9 211.8c3 -3 4.19922 -7.2998 3.19922 -11.5l-22.5996 -90.5c-1.40039 -5.39941 -6.2002 -9.09961 -11.7002 -9.09961h-0.899414
c-5.80078 0.5 -10.5 5.09961 -11 10.8994l-4.80078 52.3008l-52.2998 4.7998c-5.7998 0.5 -10.3994 5.2002 -10.8994 11c-0.400391 5.89941 3.39941 11.2002 9.09961 12.5996l90.5 22.7002c4.2002 1 8.40039 -0.200195 11.4004 -3.2002zM247.6 236.9
c-0.0996094 0 -6.39941 -1.80078 -11.3994 3.19922c-3 3 -4.2002 7.30078 -3.2002 11.4004l22.5996 90.5c1.40039 5.7002 7 9.2002 12.6006 9.09961c5.7998 -0.5 10.5 -5.09961 11 -10.8994l4.7998 -52.2998l52.2998 -4.80078c5.7998 -0.5 10.4004 -5.19922 10.9004 -11
c0.399414 -5.89941 -3.40039 -11.1992 -9.10059 -12.5996zM299.6 148.4c29.1006 29.0996 53 59.5996 65.3008 83.7998c4.89941 9.2998 17.5996 9.89941 23.3994 1.7002c27.7002 -38.9004 6.10059 -106.9 -30.5996 -143.7s-104.8 -58.2998 -143.7 -30.6006
c-8.2998 5.90039 -7.5 18.6006 1.7002 23.4004c24.2002 12.5 54.7998 36.2998 83.8994 65.4004z" />
<glyph glyph-name="grin-stars" unicode="&#xf587;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM125.7 200.9l6.09961 34.8994l-25.3994 24.6006
c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006l34.9004 -5c6.19922 -1 8.7998 -8.69922 4.2998 -13.1992l-25.4004 -24.6006l6 -34.8994c1 -6.2002 -5.39941 -11 -11 -7.90039
l-31.2998 16.2998l-31.2998 -16.2998c-5.60059 -3.09961 -12 1.7002 -11 7.90039zM385.4 273.6c6.19922 -1 8.89941 -8.59961 4.39941 -13.1992l-25.3994 -24.6006l6 -34.8994c1 -6.2002 -5.40039 -11 -11 -7.90039l-31.3008 16.2998l-31.2998 -16.2998
c-5.59961 -3.09961 -12 1.7002 -11 7.90039l6 34.8994l-25.3994 24.6006c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006z" />
<glyph glyph-name="grin-tears" unicode="&#xf588;" horiz-adv-x="640"
d="M117.1 191.9c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998c16.8994 16.9004 75.0996 26.9004 100.899 30.6006
zM623.8 161.3c21.9004 -21.8994 21.5 -57.8994 -0.799805 -80.2002c-22.4004 -22.3994 -58.4004 -22.7998 -80.2998 -0.899414c-16.9004 16.8994 -26.9004 75.0996 -30.6006 100.899c-0.899414 6.30078 4.5 11.7002 10.8008 10.8008
c25.7998 -3.7002 84 -13.7002 100.899 -30.6006zM497.2 99.5996c12.3994 -37.2998 25.0996 -43.7998 28.2998 -46.5c-44.5996 -65.7998 -120 -109.1 -205.5 -109.1s-160.9 43.2998 -205.5 109.1c3.09961 2.60059 15.7998 9.10059 28.2998 46.5
c33.4004 -63.8994 100.3 -107.6 177.2 -107.6s143.8 43.7002 177.2 107.6zM122.7 223.5c-2.40039 0.299805 -5 2.5 -49.5 -6.90039c12.3994 125.4 118.1 223.4 246.8 223.4s234.4 -98 246.8 -223.5c-44.2998 9.40039 -47.3994 7.2002 -49.5 7
c-15.2002 95.2998 -97.7998 168.5 -197.3 168.5s-182.1 -73.2002 -197.3 -168.5zM320 48c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996
c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM450.3 216.3c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM240 258.6
c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
<glyph glyph-name="grin-tongue" unicode="&#xf589;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32
s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="grin-tongue-squint" unicode="&#xf58a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998
l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 3 11.5 1.90039 15.2998 -2.5zM214.2 250.3
c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5
c3.89941 4.5 10.2998 5.5 15.2998 2.5z" />
<glyph glyph-name="grin-tongue-wink" unicode="&#xf58b;" horiz-adv-x="496"
d="M152 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
c-3.7998 -3.39941 -9.2998 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM328 320c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80s-80 35.7998 -80 80s35.7998 80 80 80zM328 192
c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM328 264c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248z
M312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998
c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3
c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998
c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998z" />
<glyph glyph-name="grin-wink" unicode="&#xf58c;" horiz-adv-x="496"
d="M328 268c25.6904 0 55.8799 -16.9199 59.8701 -42.1201c1.72949 -11.0898 -11.3506 -18.2695 -19.8301 -10.8398l-9.5498 8.47949c-14.8105 13.1904 -46.1602 13.1904 -60.9707 0l-9.5498 -8.47949c-8.33008 -7.40039 -21.5801 -0.379883 -19.8301 10.8398
c3.98047 25.2002 34.1699 42.1201 59.8604 42.1201zM168 208c-17.6699 0 -32 14.3301 -32 32s14.3301 32 32 32s32 -14.3301 32 -32s-14.3301 -32 -32 -32zM353.55 143.36c10.04 3.13965 19.3906 -5.4502 17.71 -15.3408
c-7.92969 -47.1494 -71.3193 -80.0195 -123.26 -80.0195s-115.33 32.8701 -123.26 80.0195c-1.69043 9.9707 7.76953 18.4707 17.71 15.3408c25.9297 -8.31055 64.3994 -13.0605 105.55 -13.0605s79.6201 4.75977 105.55 13.0605zM248 440c136.97 0 248 -111.03 248 -248
s-111.03 -248 -248 -248s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
<glyph glyph-name="kiss" unicode="&#xf596;" horiz-adv-x="496"
d="M168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM248 440c137 0 248 -111 248 -248
s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z
" />
<glyph glyph-name="kiss-beam" unicode="&#xf597;" horiz-adv-x="496"
d="M168 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM328 296
c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
<glyph glyph-name="kiss-wink-heart" unicode="&#xf598;" horiz-adv-x="504"
d="M304 139.5c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002
c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM374.5 223c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-2.5 -2.2998 -7.90039 -4.7002 -13.7002 -1.59961c-4.39941 2.39941 -6.89941 7.39941 -6.09961 12.3994
c3.89941 25.2002 34.2002 42.1006 59.7998 42.1006s55.7998 -16.9004 59.7998 -42.1006c0.799805 -5 -1.7002 -10 -6.09961 -12.3994c-4.40039 -2.40039 -9.90039 -1.7002 -13.7002 1.59961zM136 239.5c0 17.7002 14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32zM501.1 45.5c9.2002 -23.9004 -4.39941 -49.4004 -28.5 -55.7002l-83 -21.5c-5.39941 -1.39941 -10.8994 1.7998 -12.3994 7.10059l-22.9004 82.5996c-6.59961 24 8.7998 48.5996 34 52.5996c22 3.5 43.1006 -11.5996 49 -33l2.2998 -8.39941
l8.40039 2.2002c21.5996 5.59961 45.0996 -5.10059 53.0996 -25.9004zM334 11.7002c17.7002 -64 10.9004 -39.5 13.4004 -46.7998c-30.5 -13.4004 -64 -20.9004 -99.4004 -20.9004c-137 0 -248 111 -248 248s111 248 248 248s248 -111 247.9 -248
c0 -31.7998 -6.2002 -62.0996 -17.1006 -90c-6 1.5 -12.2002 2.7998 -18.5996 2.90039c-5.60059 9.69922 -13.6006 17.5 -22.6006 23.8994c6.7002 19.9004 10.4004 41.1006 10.4004 63.2002c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200
c30.7998 0 59.9004 7.2002 86 19.7002z" />
<glyph glyph-name="laugh" unicode="&#xf599;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM168 224
c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-beam" unicode="&#xf59a;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.700195 -8.5 -10.7998 -11.8994 -14.8994 -4.5
l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006c-12.3008 0 -23.8008 -7.90039 -31.5 -21.6006l-9.5 -17c-4.10059 -7.39941 -15.6006 -4.09961 -14.9004 4.5c3.2998 42.1006 32.2002 71.4004 56 71.4004zM127 220.1c-4.2002 -7.39941 -15.7002 -4 -15.0996 4.5
c3.2998 42.1006 32.1992 71.4004 56 71.4004c23.7998 0 52.6992 -29.2998 56 -71.4004c0.699219 -8.5 -10.8008 -11.8994 -14.9004 -4.5l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006s-23.7998 -7.90039 -31.5 -21.6006zM362.4 160c8.19922 0 14.5 -7 13.5 -15
c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-squint" unicode="&#xf59b;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM343.6 252l33.6006 -40.2998c8.59961 -10.4004 -3.90039 -24.7998 -15.4004 -18l-80 48
c-7.7998 4.7002 -7.7998 15.8994 0 20.5996l80 48c11.6006 6.7998 24 -7.7002 15.4004 -18zM134.2 193.7c-11.6006 -6.7998 -24.1006 7.59961 -15.4004 18l33.6006 40.2998l-33.6006 40.2998c-8.59961 10.2998 3.7998 24.9004 15.4004 18l80 -48
c7.7998 -4.7002 7.7998 -15.8994 0 -20.5996zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="laugh-wink" unicode="&#xf59c;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 284c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008
l-9.5 8.5c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996zM168 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z
M362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
<glyph glyph-name="meh-blank" unicode="&#xf5a4;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
s-32 14.2998 -32 32s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="meh-rolling-eyes" unicode="&#xf5a5;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM336 296c39.7998 0 72 -32.2002 72 -72s-32.2002 -72 -72 -72
s-72 32.2002 -72 72s32.2002 72 72 72zM336 184c22.0996 0 40 17.9004 40 40c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998
c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998c0 -22.0996 17.9004 -40 40 -40zM232 224c0 -39.7998 -32.2002 -72 -72 -72s-72 32.2002 -72 72s32.2002 72 72 72s72 -32.2002 72 -72zM120 224c0 -22.0996 17.9004 -40 40 -40s40 17.9004 40 40
c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998zM312 96
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
<glyph glyph-name="sad-cry" unicode="&#xf5b3;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM392 53.5996c34.5996 35.9004 56 84.7002 56 138.4c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -53.7002 21.4004 -102.4 56 -138.4v114.4
c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-151.4c28.5 -15.5996 61.2002 -24.5996 96 -24.5996s67.5 9 96 24.5996v151.4c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-114.4zM205.8 213.5c-5.7998 -3.2002 -11.2002 -0.700195 -13.7002 1.59961l-9.5 8.5
c-14.7998 13.2002 -46.1992 13.2002 -61 0l-9.5 -8.5c-3.7998 -3.39941 -9.2998 -4 -13.6992 -1.59961c-4.40039 2.40039 -6.90039 7.40039 -6.10059 12.4004c3.90039 25.1992 34.2002 42.0996 59.7998 42.0996c25.6006 0 55.8008 -16.9004 59.8008 -42.0996
c0.799805 -5 -1.7002 -10 -6.10059 -12.4004zM344 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
c-3.7998 -3.39941 -9.2002 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM248 176c30.9004 0 56 -28.7002 56 -64s-25.0996 -64 -56 -64s-56 28.7002 -56 64s25.0996 64 56 64z" />
<glyph glyph-name="sad-tear" unicode="&#xf5b4;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM256 144c38.0996 0 74 -16.7998 98.5 -46.0996
c8.5 -10.2002 7.09961 -25.3008 -3.09961 -33.8008c-10.6006 -8.7998 -25.7002 -6.69922 -33.8008 3.10059c-15.2998 18.2998 -37.7998 28.7998 -61.5996 28.7998c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM162.4 173.2c2.7998 3.7002 8.39941 3.7002 11.1992 0c11.4004 -15.2998 36.4004 -50.6006 36.4004 -68.1006
c0 -22.6992 -18.7998 -41.0996 -42 -41.0996s-42 18.4004 -42 41.0996c0 17.5 25 52.8008 36.4004 68.1006z" />
<glyph glyph-name="smile-beam" unicode="&#xf5b8;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM332 135.4c8.5 10.1992 23.5996 11.5 33.7998 3.09961
c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.2002 8.40039 25.2998 7.09961 33.7998 -3.09961
c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004zM136.5 237l-9.5 -17c-1.90039 -3.2002 -5.90039 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996zM328 296c23.7998 0 52.7002 -29.2998 56 -71.4004
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002
c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
<glyph glyph-name="surprise" unicode="&#xf5c2;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 168c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64
s-64 28.7002 -64 64s28.7002 64 64 64zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
<glyph glyph-name="tired" unicode="&#xf5c8;" horiz-adv-x="496"
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5
l-33.6006 -40.2998l33.6006 -40.2998c3.7998 -4.5 3.7002 -11 -0.100586 -15.5c-3.5 -4.10059 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 2.90039 11.5 1.90039 15.2998 -2.5z
M220 240c0 -4.2002 -2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7998 -1.60059 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7998 4.5 -3.7002 11 0.100586 15.5
c3.7998 4.40039 10.2998 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998zM248 176c45.4004 0 100.9 -38.2998 107.8 -93.2998c1.5 -11.9004 -7 -21.6006 -15.5 -17.9004c-22.7002 9.7002 -56.2998 15.2002 -92.2998 15.2002
s-69.5996 -5.5 -92.2998 -15.2002c-8.60059 -3.7002 -17 6.10059 -15.5 17.9004c6.89941 55 62.3994 93.2998 107.8 93.2998z" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 141 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 820 KiB

@@ -0,0 +1,28 @@
{% extends 'layout.html' %}
{% block slider %}
<div id="sidebar" class="w3-sidebar w3-bar-block w3-animate-left w3-dark-medium">
{% block sidebar %}{% endblock %}
</div>
{% endblock %}
{% block main %}
<div id="main">
{% block header %}{% endblock %}
<div class="content">
{% block content %}{% endblock %}
</div>
{% block footer %}{% endblock %}
</div>
{% endblock %}
{% block js_footer %}
<script>
const widthSidebar = "{{ conf.sidebar_width }}";
const sidebar = sId("sidebar");
const main = sId("main");
function toggleSidebar() { sidebarAction(widthSidebar, sidebar, main); }
sidebarOpen(widthSidebar, sidebar, main);
</script>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends 'dashboard/base.html' %}
{% load i18n %}
{% block main_content %}
<div class="w3-black w3-border w3-div-center" style="margin: 10em; height: 20em">
{% block main-bar %}
<div class="w3-orange">
<div class="w3-bar">
<div class="w3-bar-item">{% block bar-title %}{% trans "Error: missing page." %}{% endblock %}</div>
<div class="w3-right">
<a href="/" class="w3-bar-item w3-button w3-right">&times;</a>
</div>
</div>
</div>
{% endblock %}
<h4 class="w3-padding">
{% trans "This page does not exist.<br>Please re-enter a new address." %}
</h4>
</div>
{% endblock %}
@@ -0,0 +1,23 @@
{% extends 'dashboard/base.html' %}
{% load i18n %}
{% block main_content %}
<div class="w3-black w3-border w3-div-center" style="margin: 10em; height: 20em">
{% block main-bar %}
<div class="w3-orange">
<div class="w3-bar">
<div class="w3-bar-item">{% block bar-title %}{% trans "System error" %}{% endblock %}</div>
<div class="w3-right">
<a href="/" class="w3-bar-item w3-button w3-right">&times;</a>
</div>
</div>
</div>
{% endblock %}
<h4 class="w3-padding">
{% trans "This page server can't run correctly.<br>Please contact the site administrator." %}
<h6 class="w3-margin">{{ error }}</h6>
</h4>
</div>
{% endblock %}
@@ -0,0 +1,13 @@
{% if messages %}
<div class="alert">
{% for message in messages %}
<div class="w3-bar {% if message.tags %}{{ message.tags }}{% endif %}" style="width: 100%" >
<div class="w3-bar-item">{{ message }}</div>
<div class="w3-bar-item w3-button w3-right" onclick="this.parentElement.style.display='none'">&times;</div>
</div>
{% endfor %}
</div>
{% endif %}
@@ -0,0 +1,7 @@
<div class="w3-modal" id="imagemodal" style="z-index: 200;">
<div class="w3-modal-content w3-border w3-dark-medium">
<button onclick="sId('imagemodal').style.display='none'" class="w3-btn w3-display-topright w3-xlarge">&#x2716;</button>
<img src="" id="imagepreview" style="width: 100%;min-height: 150px;">
</div>
</div>
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Test Tube Scanner{% endblock %}</title>
{% block metas %}
<meta name="description" content="{% block description %}{% endblock %}">
<meta name="author" content="<dnix-421">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link href="/static/img/logo.png" rel="shortcut icon">
{% endblock %}
<link href="/static/css/w3.css" rel="stylesheet">
<link href="/static/css/all.min.css" rel="stylesheet">
<link href="/static/css/w3-custom.css" rel="stylesheet">
{% block styles %}{% endblock %}
<script src="/static/js/w3-custom.js"></script>
{% block js_header %}{% endblock %}
</head>
<body id="top-body" class="w3-black" style="margin: 0; padding: 0;">
{% block main_content %}
{% block slider %}{% endblock %}
{% block main %}
{% block header %}{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}
{% endblock %}
{% endblock %}
{% block modal %}{% endblock %}
{% block js_footer %}{% endblock %}
</body>
</html>
@@ -0,0 +1,131 @@
# encoding: utf-8
import time, json
from datetime import datetime, timezone
from django import template
from django.utils.html import format_html #, escape
register = template.Library()
@register.simple_tag
def icon_name(obj, name=None):
if not name:
name = obj.name
color, icon = '#FF0000', '&#31;'
if obj.icon:
color, icon = obj.color, obj.icon.html
result = f"""<span style="color:{color}">{icon}</span><span>&nbsp;{name}</span>"""
return format_html(result)
@register.simple_tag
def x_range(start, end=None, step=1):
"""
Usage:
{% x_range 1 5 as my_range %}
ou
{% x_range 0 10 2 as my_range %}
Retourne une liste Python similaire à range(start, end, step).
"""
try:
# convertir en entiers
if end is None:
# seul un argument passé -> range(0, start)
start = int(start)
seq = list(range(start))
else:
start = int(start)
end = int(end)
step = int(step)
seq = list(range(start, end, step))
except (ValueError, TypeError):
seq = []
return seq
@register.filter
def epoch(value):
try:
return int(time.mktime(value.timetuple())*1000)
except AttributeError:
return ''
@register.filter
def to_int(value):
return int(value)
@register.simple_tag
def math_inc(value):
return value + 1
@register.simple_tag
def math_dec(value):
return value - 1
@register.simple_tag
def math_sub(value, arg):
return value - arg
@register.simple_tag
def math_add(value, arg):
return value + arg
@register.simple_tag
def math_mul(value, arg):
return value * arg
@register.simple_tag
def math_div(value, arg):
return value / arg
@register.simple_tag
def define(val=None):
return val
@register.simple_tag
def concat(*args):
c = ""
for arg in args:
c += str(arg)
return c
@register.filter
def nope(value):
try:
return not int(value)
except:
return False
@register.simple_tag
def dict_to_json(dictionary):
return json.dumps(dictionary)
@register.simple_tag
def from_dict(dictionary, key):
return dictionary.get(key)
@register.simple_tag
def from_list(lst, key):
try:
return lst[key]
except:
return ""
@register.simple_tag
def from_choices(dictionary, key):
for k, v in dictionary:
if k == key:
return v
return ''
@register.simple_tag
def in_intlist(lst, key):
return key in lst
@register.simple_tag
def in_charlist(lst, key):
return str(key) in lst
+52
View File
@@ -0,0 +1,52 @@
"""
URL configuration for monitoring project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.i18n import i18n_patterns
from django.contrib.auth import views as auth_views
from django.conf import settings
from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
from django.views.generic import RedirectView
from . import views
handler404 = views.handler404
handler500 = views.handler500
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
]
urlpatterns += i18n_patterns(
path('admin/', admin.site.urls),
path('login/', auth_views.LoginView.as_view(template_name='admin/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('', RedirectView.as_view(url='/scanner/main/', permanent=True), name='redirect_to_mainboard'),
path('scanner/', include('scanner.urls', namespace='scanner')),
)
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
else:
urlpatterns += i18n_patterns(
re_path(r'^media/(?P<path>.*)$', serve,{'document_root': settings.MEDIA_ROOT}),
re_path(r'^static/(?P<path>.*)$', serve,{'document_root': settings.STATIC_ROOT}),
)
+11
View File
@@ -0,0 +1,11 @@
#
# encoding: utf-8
from django.shortcuts import render
def handler404(request, *args, **argv):
return render(request, 'inc/404.html', status=404)
def handler500(request, *args, **argv):
return render(request, 'inc/500.html', status=500)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for home_automation project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'home.settings')
application = get_wsgi_application()
+22
View File
@@ -0,0 +1,22 @@
#!../.venv/bin/python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'home.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
@@ -0,0 +1,304 @@
"""
Interface abstraite de capture vidéo.
Définit le contrat que toutes les implémentations doivent respecter.
4 méthodes @abstractmethod à implémenter : open(), close(), capture_frame(), is_available()
Boucle de capture dans un thread daemon avec compensation de latence pour tenir les 5 fps
Callback set_frame_callback(fn) appelé à chaque frame avec (bytes, datetime)
save_frame() avec horodatage, start()/stop(), gestionnaire de contexte (with)
CaptureError exception dédiée
"""
import os
os.environ['OPENCV_LOG_LEVEL']="0"
os.environ['OPENCV_FFMPEG_LOGLEVEL']="0"
import cv2
import numpy as np
import abc
import time
import threading
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Callable, TYPE_CHECKING
if TYPE_CHECKING:
from .circular_crop import CircularCrop # Evite l'import circulaire au runtime
logger = logging.getLogger(__name__)
class CaptureError(Exception):
"""Exception levée lors d'une erreur de capture."""
pass
class VideoCaptureInterface(abc.ABC):
"""
Interface abstraite pour la capture d'images vidéo en JPEG.
Cadence cible : 5 images par seconde (configurable).
Les sous-classes doivent implémenter les méthodes abstraites
pour gérer le matériel spécifique.
"""
# Cadence par défaut en images par seconde
DEFAULT_FPS: float = 5.0
def __init__(self, fps: float = DEFAULT_FPS):
"""
Initialise l'interface de capture.
:param fps: Cadence cible en images par seconde
"""
self._fps: float = fps
self._interval: float = 1.0 / fps # Intervalle en secondes entre chaque capture
self._running: bool = False # Indique si la capture est en cours
self._thread: Optional[threading.Thread] = None
self._frame_count: int = 0 # Compteur total d'images capturées
self._on_frame: Optional[Callable[[bytes, datetime], None]] = None # Callback image
self._circular_crop: Optional["CircularCrop"] = None # Recadrage circulaire optionnel
self._active_median = False
# ------------------------------------------------------------------
# Méthodes abstraites — obligatoires dans les sous-classes
# ------------------------------------------------------------------
@abc.abstractmethod
def open(self) -> None:
"""
Ouvre et initialise le périphérique de capture.
Doit lever CaptureError si le périphérique n'est pas disponible.
"""
@abc.abstractmethod
def close(self) -> None:
"""
Libère le périphérique de capture et toutes les ressources associées.
"""
@abc.abstractmethod
def capture_frame(self) -> bytes:
"""
Capture une seule image et la retourne en JPEG brut.
:return: Données JPEG de l'image sous forme de bytes
:raises CaptureError: Si la capture échoue
"""
@abc.abstractmethod
def is_available(self) -> bool:
"""
Vérifie si le périphérique est prêt à capturer.
:return: True si le périphérique est opérationnel
"""
# ------------------------------------------------------------------
# Méthodes concrètes communes à toutes les implémentations
# ------------------------------------------------------------------
@property
def fps(self) -> float:
"""Cadence actuelle en images par seconde."""
return self._fps
@fps.setter
def fps(self, value: float) -> None:
"""Modifie la cadence de capture à la volée."""
if value <= 0:
raise ValueError("La cadence doit être un nombre positif")
self._fps = value
self._interval = 1.0 / value
logger.debug("Cadence mise à jour : %.1f fps (intervalle %.3f s)", value, self._interval)
@property
def frame_count(self) -> int:
"""Nombre total d'images capturées depuis le démarrage."""
return self._frame_count
def set_frame_callback(self, callback: Callable[[bytes, datetime], None]) -> None:
"""
Définit la fonction appelée à chaque nouvelle image capturée.
:param callback: Fonction(jpeg_bytes, timestamp) appelée pour chaque frame
"""
self._on_frame = callback
def start(self) -> None:
"""
Démarre la capture en continu dans un thread dédié.
Appelle open() si le périphérique n'est pas encore disponible.
"""
if self._running:
logger.warning("La capture est déjà en cours")
return
if not self.is_available():
logger.info("Ouverture du périphérique avant démarrage")
self.open()
self._running = True
self._frame_count = 0
self._thread = threading.Thread(
target=self._capture_loop,
name=f"{self.__class__.__name__}-capture",
daemon=True, # Thread démon : s'arrête avec le processus principal
)
self._thread.start()
logger.info("%s : capture démarrée à %.1f fps", self.__class__.__name__, self._fps)
def stop(self) -> None:
"""
Arrête la capture et attend la fin du thread.
Appelle close() pour libérer les ressources.
"""
if not self._running:
return
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5.0) # Attente max 5 secondes
self.close()
logger.info(
"%s : capture arrêtée — %d images capturées",
self.__class__.__name__,
self._frame_count,
)
def set_circular_crop(self, crop: Optional["CircularCrop"]) -> None:
"""
Active ou désactive le recadrage circulaire appliqué à chaque frame.
Lorsqu'un CircularCrop est défini, chaque appel à capture_frame()
passe automatiquement par process_frame() avant d'être transmis
au callback ou sauvegardé.
:param crop: Instance CircularCrop configurée, ou None pour désactiver
"""
self._circular_crop = crop
if crop is not None:
logger.info(
"%s : recadrage circulaire activé (R=%d, stratégie=%s)",
self.__class__.__name__, crop.radius, crop.strategy.name,
)
else:
logger.info("%s : recadrage circulaire désactivé", self.__class__.__name__)
def process_frame(self, jpeg_bytes: bytes) -> bytes:
"""
Applique le post-traitement configuré sur une image brute.
Actuellement : recadrage circulaire si un CircularCrop est défini.
Peut être surchargé dans une sous-classe pour des traitements spécifiques.
:param jpeg_bytes: Image JPEG brute issue du capteur
:return: Image traitée (JPEG ou PNG selon la stratégie)
"""
if self._circular_crop is not None:
return self._circular_crop.process(jpeg_bytes)
return jpeg_bytes
def save_frame(self, jpeg_bytes: bytes, directory: str = ".", prefix: str = "frame") -> Path:
"""
Enregistre une image JPEG sur le disque avec un horodatage.
:param jpeg_bytes: Données brutes JPEG
:param directory: Dossier de destination
:param prefix: Préfixe du nom de fichier
:return: Chemin du fichier créé
"""
dest = Path(directory)
dest.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
filepath = dest / f"{prefix}_{timestamp}.jpg"
filepath.write_bytes(jpeg_bytes)
logger.debug("Image sauvegardée : %s (%d octets)", filepath, len(jpeg_bytes))
return filepath
def __enter__(self) -> "VideoCaptureInterface":
"""Permet l'utilisation avec le gestionnaire de contexte 'with'."""
self.open()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Ferme proprement le périphérique à la sortie du bloc 'with'."""
self.close()
def __repr__(self) -> str:
status = "actif" if self._running else "arrêté"
return f"<{self.__class__.__name__} fps={self._fps} status={status}>"
# ------------------------------------------------------------------
# tracer médianes
# ------------------------------------------------------------------
def set_median(self, is_median=False):
"""
Active ou désactive les médianes
"""
self._active_median = is_median
def display_median(self, jpeg):
if self._active_median:
nparr = np.frombuffer(jpeg, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
height, width = frame.shape[:2]
center_x = width // 2
center_y = height // 2
cv2.line(frame, (center_x, 0), (center_x, height), (0, 255, 0), 1)
cv2.line(frame, (0, center_y), (width, center_y), (0, 255, 0), 1)
cv2.circle(frame, (center_x, center_y), 2, (0, 0, 255), -1)
cv2.putText(frame, f"Num: {self._frame_count}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
_, buffer = cv2.imencode('.jpg', frame)
jpeg_bytes = buffer.tobytes()
return jpeg_bytes
return jpeg
# ------------------------------------------------------------------
# Boucle interne de capture (privée)
# ------------------------------------------------------------------
def _capture_loop(self) -> None:
"""
Boucle principale de capture tournant dans le thread dédié.
Respecte la cadence cible et appelle le callback si défini.
"""
while self._running:
loop_start = time.monotonic()
try:
jpeg = self.capture_frame()
jpeg = self.display_median(jpeg)
jpeg = self.process_frame(jpeg) # Recadrage circulaire si configuré
self._frame_count += 1
ts = datetime.now(timezone.utc)
if self._on_frame:
try:
self._on_frame(jpeg, ts)
except Exception as cb_err: # noqa: BLE001
logger.error("Erreur dans le callback image : %s", cb_err)
except CaptureError as err:
logger.error("Échec de capture (#%d) : %s", self._frame_count, err)
# Compensation du temps d'exécution pour tenir la cadence
elapsed = time.monotonic() - loop_start
sleep_time = self._interval - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
else:
logger.debug(
"Cadence non tenue : %.3f s de retard (traitement=%.3f s)",
-sleep_time,
elapsed,
)
+245
View File
@@ -0,0 +1,245 @@
"""
Utilitaire de recadrage circulaire centré sur une image JPEG.
Trois stratégies disponibles :
- MASK_BLACK : image originale, pixels hors cercle mis à noir, sortie JPEG
- CROP_PNG : carré 2R×2R centré, canal alpha = masque circulaire, sortie PNG
- CROP_JPEG : carré 2R×2R centré sans transparence, sortie JPEG (le plus compact)
Masque noir : image JPEG de taille originale, pixels hors cercle = noir → simple mais pas économe
Crop circulaire + PNG : on crop au carré 2R×2R, on applique le masque alpha → PNG plus petit, transparence vraie, mais PNG > JPEG en taille
Crop carré JPEG : on extrait juste le carré 2R×2R centré → JPEG compact, pas de transparence
"""
import io
import logging
from enum import Enum, auto
from typing import Optional
import numpy as np
logger = logging.getLogger(__name__)
class CropStrategy(Enum):
"""Stratégie de recadrage circulaire."""
MASK_BLACK = auto() # Masque noir — taille originale, sortie JPEG
CROP_PNG = auto() # Carré cropé + alpha circulaire — sortie PNG
CROP_JPEG = auto() # Carré cropé sans alpha — sortie JPEG (défaut recommandé)
class CircularCrop:
"""
Applique un recadrage circulaire centré sur une image fournie en bytes JPEG.
Utilise uniquement NumPy + Pillow pour rester léger et compatible
aussi bien sur PC que sur Raspberry Pi.
Exemple ::
crop = CircularCrop(radius=200, strategy=CropStrategy.CROP_JPEG, quality=80)
result_bytes = crop.process(jpeg_bytes)
"""
def __init__(
self,
radius: int,
strategy: CropStrategy = CropStrategy.CROP_JPEG,
jpeg_quality: int = 85,
center: Optional[tuple[int, int]] = None,
):
"""
:param radius: Rayon du cercle de recadrage en pixels
:param strategy: Stratégie de sortie (voir CropStrategy)
:param jpeg_quality: Qualité JPEG pour les sorties JPEG [0-100]
:param center: Centre du cercle (col, row) — None = centre de l'image
"""
if radius <= 0:
raise ValueError("Le rayon doit être un entier strictement positif")
if not 0 <= jpeg_quality <= 100:
raise ValueError("La qualité JPEG doit être comprise entre 0 et 100")
self._radius = radius
self._strategy = strategy
self._jpeg_quality = jpeg_quality
self._center = center # None = calcul automatique au premier appel
# Cache du masque pour éviter de le recalculer à chaque frame
self._mask_cache: Optional[np.ndarray] = None
self._mask_shape: Optional[tuple[int, int, int]] = None # (H, W, strategy)
# ------------------------------------------------------------------
# API publique
# ------------------------------------------------------------------
def process(self, jpeg_bytes: bytes) -> bytes:
"""
Applique le recadrage circulaire sur une image JPEG.
:param jpeg_bytes: Image source en bytes JPEG
:return: Image recadrée selon la stratégie choisie (JPEG ou PNG)
:raises ValueError: Si les bytes ne sont pas une image valide
"""
from PIL import Image
# Décodage JPEG → tableau NumPy RGB
img = Image.open(io.BytesIO(jpeg_bytes)).convert("RGB")
arr = np.asarray(img, dtype=np.uint8) # shape (H, W, 3)
h, w = arr.shape[:2]
cx, cy = self._resolve_center(w, h)
if self._strategy == CropStrategy.MASK_BLACK:
return self._apply_mask_black(arr, cx, cy)
elif self._strategy == CropStrategy.CROP_PNG:
return self._apply_crop_png(arr, cx, cy, w, h)
else: # CROP_JPEG par défaut
return self._apply_crop_jpeg(arr, cx, cy, w, h)
@property
def radius(self) -> int:
return self._radius
@radius.setter
def radius(self, value: int) -> None:
"""Modifie le rayon et invalide le cache du masque."""
if value <= 0:
raise ValueError("Le rayon doit être un entier strictement positif")
self._radius = value
self._invalidate_cache()
@property
def strategy(self) -> CropStrategy:
return self._strategy
@strategy.setter
def strategy(self, value: CropStrategy) -> None:
self._strategy = value
self._invalidate_cache()
@property
def jpeg_quality(self) -> int:
return self._jpeg_quality
@jpeg_quality.setter
def jpeg_quality(self, value: int) -> None:
if not 0 <= value <= 100:
raise ValueError("La qualité JPEG doit être comprise entre 0 et 100")
self._jpeg_quality = value
# ------------------------------------------------------------------
# Stratégies internes
# ------------------------------------------------------------------
def _apply_mask_black(self, arr: np.ndarray, cx: int, cy: int) -> bytes:
"""
Pixels hors cercle remplacés par du noir.
Sortie : JPEG de la même taille que l'original.
"""
from PIL import Image
mask = self._get_circle_mask(arr.shape[:2], cx, cy) # shape (H, W) bool
result = arr.copy()
result[~mask] = 0 # Tout ce qui est hors cercle → noir RGB
buf = io.BytesIO()
Image.fromarray(result).save(buf, format="JPEG", quality=self._jpeg_quality)
return buf.getvalue()
def _apply_crop_png(self, arr: np.ndarray, cx: int, cy: int, w: int, h: int) -> bytes:
"""
Crop carré 2R×2R centré + canal alpha circulaire.
Sortie : PNG avec transparence (pixels hors cercle = transparent).
"""
from PIL import Image
x1, y1, x2, y2 = self._crop_box(cx, cy, w, h)
cropped = arr[y1:y2, x1:x2] # shape (2R, 2R, 3) ou moins si bord
# Canal alpha : 255 dans le cercle, 0 à l'extérieur
ch, cw = cropped.shape[:2]
local_cx = cx - x1
local_cy = cy - y1
alpha_mask = self._get_circle_mask((ch, cw), local_cx, local_cy)
alpha = np.where(alpha_mask, 255, 0).astype(np.uint8)
rgba = np.dstack([cropped, alpha]) # shape (H, W, 4)
buf = io.BytesIO()
Image.fromarray(rgba, mode="RGBA").save(buf, format="PNG", optimize=True)
return buf.getvalue()
def _apply_crop_jpeg(self, arr: np.ndarray, cx: int, cy: int, w: int, h: int) -> bytes:
"""
Crop carré 2R×2R centré, pixels hors cercle mis à noir.
Sortie : JPEG compact sans canal alpha (meilleur compromis taille/qualité).
"""
from PIL import Image
x1, y1, x2, y2 = self._crop_box(cx, cy, w, h)
cropped = arr[y1:y2, x1:x2].copy()
ch, cw = cropped.shape[:2]
local_cx = cx - x1
local_cy = cy - y1
mask = self._get_circle_mask((ch, cw), local_cx, local_cy)
cropped[~mask] = 0 # Hors cercle → noir dans le crop
buf = io.BytesIO()
Image.fromarray(cropped).save(buf, format="JPEG", quality=self._jpeg_quality)
return buf.getvalue()
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _resolve_center(self, w: int, h: int) -> tuple[int, int]:
"""Retourne le centre configuré ou le centre géométrique de l'image."""
if self._center is not None:
return self._center
return (w // 2, h // 2)
def _crop_box(self, cx: int, cy: int, w: int, h: int) -> tuple[int, int, int, int]:
"""
Calcule la boîte de crop 2R×2R clampée aux bords de l'image.
:return: (x1, y1, x2, y2) en coordonnées image
"""
r = self._radius
x1 = max(cx - r, 0)
y1 = max(cy - r, 0)
x2 = min(cx + r, w)
y2 = min(cy + r, h)
return (x1, y1, x2, y2)
def _get_circle_mask(self, shape: tuple[int, int], cx: int, cy: int) -> np.ndarray:
"""
Construit (ou récupère du cache) le masque booléen circulaire.
Le masque est recalculé uniquement si la taille ou le centre change.
:param shape: (hauteur, largeur) du tableau cible
:param cx: Colonne du centre dans ce tableau
:param cy: Ligne du centre dans ce tableau
:return: Tableau bool shape (H, W) — True = dans le cercle
"""
cache_key = (shape[0], shape[1], cx, cy, self._radius)
if self._mask_cache is None or self._mask_shape != cache_key:
h, w = shape
# Coordonnées entières de chaque pixel via meshgrid
ys, xs = np.ogrid[:h, :w]
dist_sq = (xs - cx) ** 2 + (ys - cy) ** 2
self._mask_cache = dist_sq <= self._radius ** 2
self._mask_shape = cache_key
logger.debug(
"Masque circulaire recalculé : shape=%s centre=(%d,%d) R=%d",
shape, cx, cy, self._radius,
)
return self._mask_cache
def _invalidate_cache(self) -> None:
"""Invalide le cache du masque (après changement de rayon ou stratégie)."""
self._mask_cache = None
self._mask_shape = None
+356
View File
@@ -0,0 +1,356 @@
'''
GCode pour piloter la L2544 Laser Engraving Machine
GRBLController:
Commande uniquement les mouvements (X, Y)
Le mode absolue est retenu
GridScanner
Balayage complet de la grille d'éprouvettes en mode serpentin
Usage:
grbl = GRBLController()
scan = GridScanner(grbl, xbase=100, ybase=100, duration=5)
scan.start()
Created on 25 mars 2026
@author: denis@miraceti.net
'''
import logging
import serial
import time
import threading
from typing import Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class GRBLController:
'''
Contrôleur pour machine de gravure laser L2544 (GRBL 1.1f)
Fonctions de base pour la calibration : déplacement manuel et gestion de la position.
'''
X_MAX = 350
Y_MAX = 250
X_MIN = 0
Y_MIN = 0
def __init__(self, port='/dev/ttyUSB0', baudrate=115200, timeout=1, send_callback=None, x_max=None, y_max=None):
self.lock = threading.Lock()
self.port = port
self.baudrate = baudrate
self.timeout = timeout
if x_max is not None:
self.X_MAX = x_max
if y_max is not None:
self.Y_MAX = y_max
self._state = send_callback
if self._state is None:
self._state = self._send_msg
self.x, self.y = 0, 0
self.start_connection()
self._wake_up()
self._init_machine()
def wait_for(self, delay=1.0):
threading.Event().wait(delay*1.0)
def _send_msg(self, **msg):
print(msg)
def clear_buffer(self):
while self.ser.in_waiting >0:
msg = self.ser.readline().decode().strip()
print(f"Buffer: {msg}")
self._state(state='serial', msg=msg)
def start_connection(self):
n = 0
while True:
try:
self.ser = serial.Serial(self.port, self.baudrate, timeout=self.timeout, exclusive=True)
# CRITIQUE :
self.ser.setDTR(False)
self.ser.setRTS(False)
self.clear_buffer()
break
except Exception as e:
print(f"Erreur de connexion (essai {n}): {e}")
n += 1
self.wait_for(1.0)
def _init_machine(self):
self.send("G21") # Unités en mm
self.send("G90") # Mode absolu
def _clamp(self, x, y):
self.clear_buffer()
x = max(self.X_MIN, min(self.X_MAX, x))
y = max(self.Y_MIN, min(self.Y_MAX, y))
return x, y
def _wake_up(self):
#with self.lock:
self.ser.write(b"\r\n\r\n")
self.wait_for(1)
self.clear_buffer()
def send(self, cmd, wait_ok=True, timeout=5):
try:
return self._send(cmd, wait_ok, timeout)
except Exception as e:
#print("Send error:", e)
self._state(state='error', msg=f"Error send {cmd} command: {e}")
self.close()
self.start_connection()
self._wake_up()
self._init_machine()
'''
self.recover()
self.reset_grbl()
raise'''
def recover(self):
#print("Récupération de GRBL...")
self._state(state='recover', msg=f"Erreur, récupération de GRBL...")
self.wait_for(1)
self._wake_up()
def _send(self, cmd, wait_ok=True, timeout=5):
#print(f">>> {cmd}")
self._state(state='send', msg=f">>> {cmd}")
self.ser.write((cmd + "\n").encode())
if not wait_ok:
return None
start = time.time()
while True:
if time.time() - start > timeout:
raise TimeoutError(f"Timeout sur la commande: {cmd}")
raw = self.ser.readline()
if not raw:
continue
line = raw.decode(errors="ignore").strip()
if not line:
continue
if line.startswith("<"):
continue # Ignorer les messages de status asynchrones
if "ok" in line.lower():
return line
if "error" in line.lower():
raise Exception(f"Erreur GRBL: {line}")
def get_status(self):
#with self.lock:
self.ser.write(b"?\n")
while True:
line = self.ser.readline()
if not line:
continue
line = line.decode().strip()
if line.startswith("<"):
return line
def reset_grbl(self):
self.send("$X") # Réinitialise les alarmes
self.wait_idle()
self.send("$H") # Homing
self.wait_idle()
def _mpos(self, status):
if "MPos" in status:
mpos = status.split("MPos:")[1].split("|")[0]
x, y, *_ = mpos.split(",")
return float(x), float(y)
return None, None
def get_mpos(self):
return self._mpos(self.get_status())
def wait_idle(self, timeout=10):
start = time.time()
while True:
if time.time() - start > timeout:
raise TimeoutError("Délai d'attente pour Idle dépassé")
status = self.get_status()
self.x, self.y = self._mpos(status)
self._state(xy=True, x=self.x, y=self.y)
if status and "Idle" in status:
break
self.wait_for(0.1)
def move_to(self, x, y, feed=1000):
x, y = self._clamp(x, y)
#cmd = f"G0 X{x:.2f} Y{y:.2f} F{feed}" # feed is not updated in G0 mode
cmd = f"G53 G1 X{x:.2f} Y{y:.2f} F{feed}"
self.send(cmd)
self.wait_idle()
def move_relative_(self, dx=0, dy=0, feed=1000):
self.send("G91") # Mode relatif
cmd = f"G0 X{dx} Y{dy} F{feed}"
self.send(cmd)
self.send("G90") # Retour en mode absolu
self.wait_idle()
def move_relative(self, dx=0, dy=0, feed=1000):
x, y = self.get_mpos() # Position actuelle
self.move_to(x + dx, y + dy)
def go_origin(self, feed=1000):
self.move_to(0, 0, feed=feed)
self.wait_for(2.0)
def set_position(self, x, y):
x, y = self._clamp(x, y)
cmd = f"G92 X{x:.2f} Y{y:.2f}"
self.send(cmd)
self.wait_for(2.0)
def move_up(self, step=10, feed=1000):
self.move_relative(dy=step, feed=feed)
def move_down(self, step=10, feed=1000):
self.move_relative(dy=-step, feed=feed)
def move_left(self, step=10, feed=1000):
self.move_relative(dx=-step, feed=feed)
def move_right(self, step=10, feed=1000):
self.move_relative(dx=step, feed=feed)
def close(self):
self.ser.close()
class GridScanner:
def __init__(self, grbl, proc=None, **config):
'''
xbase # Position X de départ (col 0) en mm
ybase # Position Y de départ (row 0) en mm
cols # Nombre de colonnes
rows # Nombre de lignes
dx # Pas entre colonnes en mm
dy # Pas entre lignes en mm
duration # Durée de filmage par éprouvette en secondes
feed # Vitesse de déplacement entre éprouvettes (mm/min)
'''
self.grbl = grbl
self.proc = proc
self.position = config.get('position', 'HG')
self.xbase = config.get('xbase', 50)
self.ybase = config.get('ybase', 50)
self.cols = config.get('cols', 6)
self.rows = config.get('rows', 4)
self.dx = config.get('dx', 20)
self.dy = config.get('dy', 19)
self.feed = config.get('feed', 1000)
self.duration = config.get('duration', 120) # secondes
self.xnext = config.get('xnext', 50)
self.ynext = config.get('ynext', 50)
row_to_char = config.get('row_to_char', 'D,C,B,A')
self.row_to_char = row_to_char.split(',')
self.stop_playing = None
def halt(self):
self.proc.record = False
return self.stop_playing.set()
def _capture(self, uuid: str, duration: float, stop_running: Optional[threading.Event]) -> None:
"""
Déclenche la caméra ArduCam et attend la fin de l'acquisition.
"""
print(f"# démarrer l'enregistrement {uuid}")
self.proc.uuid = uuid
self.proc.record = True
start = time.monotonic()
while not stop_running.is_set():
if time.monotonic() - start > duration:
break
self.grbl.wait_for(1.0)
print("# arrêter l'enregistrement")
self.proc.record = False
self.proc.uuid = None
def start(self, xnext=None, ynext=None, position=None):
"""
Balayage complet de la grille d'éprouvettes en mode serpentin.
Parcours :
- Lignes paires (0, 2) : gauche → droite (col 0 → col 5)
- Lignes impaires (1, 3) : droite → gauche (col 5 → col 0)
Le déplacement entre éprouvettes se fait en mode absolu via move_to().
La caméra filme pendant `` secondes sur chaque position.
Grille : 6 colonnes × 4 lignes = 24 éprouvettes
- x = XBASE + col * PAS_X
- y = YBASE + row * PAS_Y
"""
try:
if xnext is None:
xnext = self.xnext
if ynext is None:
ynext = self.ynext
if position is None:
position = self.position
max_cells = self.cols * self.rows
cell = 0
logger.info("Début du scan serpentin : %d éprouvettes, %d s/éprouvette, durée totale estimée : %d min",
max_cells,
self.duration,
(max_cells * self.duration) // 60,
)
self.stop_playing = threading.Event()
for row in range(self.rows):
if self.stop_playing.is_set():
break
# Ordre des colonnes selon la parité de la ligne (serpentin)
if row % 2 == 0:
# Ligne paire : gauche → droite
cols = range(self.cols)
else:
# Ligne impaire : droite → gauche
cols = range(self.cols - 1, -1, -1)
for col in cols:
if self.stop_playing.is_set():
break
# Calcul de la position absolue en mm
x = self.xbase + col * self.dx
y = self.ybase + row * self.dy
cell += 1
logger.info(
"[%02d/%02d] row=%d col=%d → X=%.1f mm Y=%.1f mm",
cell, max_cells, row, col, x, y,
)
self.grbl.move_to(x, y, feed=self.feed)
uuid = f'{self.proc.session}-{position}-{self.row_to_char[row]}{col+1}'
self._capture(uuid, self.duration, self.stop_playing)
# Retour à nexr après le scan
logger.info("Scan terminé — retour à l'origine (X=%.1f Y=%.1f)", xnext, ynext)
self.grbl.move_to(xnext, ynext, feed=self.feed*2)
except Exception as e:
logger.error(f"scan error: {e}")
@@ -0,0 +1,339 @@
"""
Implémentation de la capture vidéo pour Raspberry Pi via PiCamera2.
Dépendance : picamera2 (sudo apt install python3-picamera2)
Compatible Pi 4 / Pi 5 avec le module caméra officiel (v2, v3, HQ).
"""
import io
import logging
from typing import Optional
from .capture_interface import CaptureError, VideoCaptureInterface
logger = logging.getLogger(__name__)
class PiCamera2Capture(VideoCaptureInterface):
"""
Capture JPEG depuis la caméra officielle Raspberry Pi via PiCamera2.
Utilise le pipeline libcamera pour la capture basse latence.
Supporte les modules Camera Module v1, v2, v3 et HQ Camera.
La résolution demandée (width, height) est approchée au mieux :
libcamera sélectionne automatiquement le mode sensor dont la résolution
native est la plus proche, puis redimensionne en ISP.
Utiliser list_sensor_modes() pour connaître les modes disponibles.
Exemple d'utilisation ::
# Afficher les modes disponibles avant d'instancier
PiCamera2Capture.list_sensor_modes()
cam = PiCamera2Capture(fps=5, width=1280, height=720)
cam.set_frame_callback(lambda data, ts: print(f"{ts}: {len(data)} octets"))
cam.start()
time.sleep(10)
cam.stop()
"""
def __init__(
self,
fps: float = VideoCaptureInterface.DEFAULT_FPS,
width: int = 1280,
height: int = 720,
jpeg_quality: int = 85,
camera_index: int = 0,
use_video_config: bool = True,
):
"""
:param fps: Cadence cible en images par seconde
:param width: Largeur souhaitée en pixels (approchée au mode sensor le plus proche)
:param height: Hauteur souhaitée en pixels (approchée au mode sensor le plus proche)
:param jpeg_quality: Qualité de compression JPEG [0-100]
:param camera_index: Index de la caméra (0 par défaut, utile sur Pi 5 dual-cam)
:param use_video_config: True = VideoConfiguration (flux continu, basse latence)
False = StillConfiguration (haute résolution, plus lent)
"""
super().__init__(fps=fps)
self._width: int = width
self._height: int = height
self._jpeg_quality: int = jpeg_quality
self._camera_index: int = camera_index
self._use_video_config: bool = use_video_config
self._picam2 = None # Instance Picamera2
self._effective_size: Optional[tuple[int, int]] = None # Résolution réellement appliquée
# ------------------------------------------------------------------
# Méthode statique utilitaire — à appeler avant d'instancier
# ------------------------------------------------------------------
@staticmethod
def list_sensor_modes(camera_index: int = 0) -> list[dict]:
"""
Affiche et retourne tous les modes sensor disponibles pour la caméra.
À appeler avant d'instancier PiCamera2Capture pour choisir
une résolution compatible avec un mode sensor natif.
:param camera_index: Index de la caméra à interroger
:return: Liste de dicts décrivant chaque mode sensor
:raises CaptureError: Si picamera2 n'est pas disponible
"""
try:
from picamera2 import Picamera2
except ImportError as exc:
raise CaptureError(
"picamera2 introuvable — installez-le avec : "
"sudo apt install python3-picamera2"
) from exc
picam2 = Picamera2(camera_index)
try:
# sensor_modes doit être interrogé avant configure()
modes = picam2.sensor_modes
print(f"\n=== Modes sensor disponibles (caméra index={camera_index}) ===")
for i, mode in enumerate(modes):
size = mode.get("size", "?")
fps = mode.get("fps", "?")
crop = mode.get("crop_limits", "?")
fmt = mode.get("format", "?")
print(
f" [{i}] {size[0]}×{size[1]}px "
f"fps_max={fps:.1f} format={fmt} crop={crop}"
)
print()
return modes
finally:
picam2.close()
# ------------------------------------------------------------------
# Implémentation des méthodes abstraites
# ------------------------------------------------------------------
def open(self) -> None:
"""
Initialise PiCamera2, sélectionne le mode sensor le plus adapté
à la résolution demandée, configure le flux et démarre libcamera.
Le mode sensor est choisi en minimisant la distance euclidienne
entre la résolution native du mode et (width, height) demandés.
"""
try:
from picamera2 import Picamera2
except ImportError as exc:
raise CaptureError(
"picamera2 introuvable — installez-le avec : "
"sudo apt install python3-picamera2"
) from exc
try:
self._picam2 = Picamera2(self._camera_index)
# Sélection du mode sensor le plus proche de la résolution demandée
best_mode = self._select_best_sensor_mode(self._picam2)
# Construction de la configuration avec le mode sensor forcé
if self._use_video_config:
config = self._picam2.create_video_configuration(
main={"size": (self._width, self._height), "format": "RGB888"},
raw=best_mode, # Force le mode sensor natif
)
logger.debug("Configuration VideoConfiguration sélectionnée")
else:
config = self._picam2.create_still_configuration(
main={"size": (self._width, self._height), "format": "RGB888"},
raw=best_mode,
)
logger.debug("Configuration StillConfiguration sélectionnée")
self._picam2.configure(config)
self._picam2.start()
# Lecture de la résolution effectivement appliquée par l'ISP
actual = config["main"]["size"]
self._effective_size = actual
logger.info(
"PiCamera2 ouverte : index=%d demandé=%dx%d effectif=%dx%d "
"mode_sensor=%dx%d mode=%s",
self._camera_index,
self._width, self._height,
actual[0], actual[1],
best_mode["size"][0], best_mode["size"][1],
"video" if self._use_video_config else "still",
)
# Avertissement si la résolution effective diffère de la demande
if actual != (self._width, self._height):
logger.warning(
"Résolution ajustée par libcamera : %dx%d%dx%d. "
"Utilisez list_sensor_modes() pour connaître les tailles compatibles.",
self._width, self._height, actual[0], actual[1],
)
except Exception as exc:
if self._picam2 is not None:
try:
self._picam2.close()
except Exception: # noqa: BLE001
pass
self._picam2 = None
raise CaptureError(f"Impossible d'ouvrir PiCamera2 : {exc}") from exc
def close(self) -> None:
"""Arrête le pipeline libcamera et libère les ressources."""
if self._picam2 is not None:
try:
self._picam2.stop()
self._picam2.close()
logger.info("PiCamera2 fermée (index=%d)", self._camera_index)
except Exception as exc: # noqa: BLE001
logger.warning("Erreur lors de la fermeture de PiCamera2 : %s", exc)
finally:
self._picam2 = None
self._effective_size = None
def capture_frame(self) -> bytes:
"""
Capture une image depuis le flux libcamera et l'encode en JPEG.
Stratégie : capture_array() → tableau NumPy RGB → encodage Pillow.
capture_file() ne supporte pas le paramètre quality ; on encode
manuellement pour contrôler le taux de compression.
:return: Données JPEG brutes
:raises CaptureError: Si la capture ou l'encodage échoue
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
try:
from PIL import Image
# Récupération du tableau RGB depuis le flux libcamera
arr = self._picam2.capture_array("main") # shape (H, W, 3) uint8
# Encodage manuel en JPEG avec la qualité configurée
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="JPEG", quality=self._jpeg_quality)
return buf.getvalue()
except Exception as exc:
raise CaptureError(f"Échec de capture PiCamera2 : {exc}") from exc
def is_available(self) -> bool:
"""Retourne True si le pipeline libcamera est démarré."""
return self._picam2 is not None
# ------------------------------------------------------------------
# Méthodes spécifiques à PiCamera2
# ------------------------------------------------------------------
def capture_high_res(self, width: int, height: int) -> bytes:
"""
Capture une image haute résolution hors flux principal (photo ponctuelle).
Utile pour déclencher une capture pleine résolution pendant un flux 5 fps.
Utilise capture_request() + make_array() + encodage Pillow.
:param width: Largeur souhaitée en pixels (indicatif, dépend de la config active)
:param height: Hauteur souhaitée en pixels
:return: Données JPEG brutes
:raises CaptureError: Si PiCamera2 n'est pas initialisée
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
try:
from PIL import Image
# Capture d'une requête unique depuis le flux actif
request = self._picam2.capture_request()
arr = request.make_array("main") # shape (H, W, 3) uint8 RGB
request.release()
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="JPEG", quality=self._jpeg_quality)
return buf.getvalue()
except Exception as exc:
raise CaptureError(f"Échec de capture haute résolution : {exc}") from exc
def set_controls(self, **kwargs) -> None:
"""
Applique des contrôles libcamera directement (exposition, gain, balance des blancs…).
Exemple ::
cam.set_controls(ExposureTime=10000, AnalogueGain=2.0)
:param kwargs: Contrôles libcamera valides pour le module connecté
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
self._picam2.set_controls(kwargs)
logger.debug("Contrôles appliqués : %s", kwargs)
def get_camera_properties(self) -> dict:
"""
Retourne les métadonnées du module caméra détecté.
:return: Dictionnaire des propriétés (modèle, résolution max, etc.)
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
return self._picam2.camera_properties
@property
def jpeg_quality(self) -> int:
"""Qualité JPEG [0-100]."""
return self._jpeg_quality
@jpeg_quality.setter
def jpeg_quality(self, value: int) -> None:
if not 0 <= value <= 100:
raise ValueError("La qualité JPEG doit être comprise entre 0 et 100")
self._jpeg_quality = value
@property
def resolution(self) -> tuple[int, int]:
"""Résolution de capture demandée (largeur, hauteur)."""
return (self._width, self._height)
@property
def effective_resolution(self) -> Optional[tuple[int, int]]:
"""
Résolution effectivement appliquée par l'ISP après ouverture.
None si la caméra n'est pas encore ouverte.
"""
return self._effective_size
# ------------------------------------------------------------------
# Helpers privés
# ------------------------------------------------------------------
def _select_best_sensor_mode(self, picam2) -> dict:
"""
Choisit le mode sensor dont la résolution native est la plus proche
de (width, height) en minimisant la distance euclidienne.
:param picam2: Instance Picamera2 déjà créée mais pas encore configurée
:return: Dict du mode sensor sélectionné
"""
modes = picam2.sensor_modes
if not modes:
raise CaptureError("Aucun mode sensor disponible")
def distance(mode: dict) -> float:
mw, mh = mode["size"]
# Distance euclidienne normalisée entre la résolution du mode et la cible
return ((mw - self._width) ** 2 + (mh - self._height) ** 2) ** 0.5
best = min(modes, key=distance)
logger.debug(
"Mode sensor sélectionné : %dx%d (demandé : %dx%d)",
best["size"][0], best["size"][1],
self._width, self._height,
)
return best
@@ -0,0 +1,227 @@
"""
Implémentation de la capture vidéo pour Raspberry Pi via PiCamera2.
Dépendance : picamera2 (sudo apt install python3-picamera2)
Compatible Pi 4 / Pi 5 avec le module caméra officiel.
"""
import io
import logging
from typing import Optional
from .capture_interface import CaptureError, VideoCaptureInterface
logger = logging.getLogger(__name__)
class PiCamera2Capture(VideoCaptureInterface):
"""
Capture JPEG depuis la caméra officielle Raspberry Pi via PiCamera2.
Utilise le pipeline libcamera pour la capture basse latence.
Supporte les modules Camera Module v1, v2, v3 et HQ Camera.
Exemple d'utilisation ::
cam = PiCamera2Capture(fps=5, width=1280, height=720)
cam.set_frame_callback(lambda data, ts: print(f"{ts}: {len(data)} octets"))
cam.start()
time.sleep(10)
cam.stop()
"""
def __init__(
self,
fps: float = VideoCaptureInterface.DEFAULT_FPS,
width: int = 1280,
height: int = 720,
jpeg_quality: int = 85,
camera_index: int = 0,
use_video_config: bool = True,
):
"""
:param fps: Cadence cible en images par seconde
:param width: Largeur du flux de capture en pixels
:param height: Hauteur du flux de capture en pixels
:param jpeg_quality: Qualité de compression JPEG [0-100]
:param camera_index: Index de la caméra (0 par défaut, utile sur Pi 5 dual-cam)
:param use_video_config: True = configuration VideoConfiguration (flux continu)
False = StillConfiguration (haute résolution, plus lent)
"""
super().__init__(fps=fps)
self._width: int = width
self._height: int = height
self._jpeg_quality: int = jpeg_quality
self._camera_index: int = camera_index
self._use_video_config: bool = use_video_config
self._picam2 = None # Instance Picamera2
# ------------------------------------------------------------------
# Implémentation des méthodes abstraites
# ------------------------------------------------------------------
def open(self) -> None:
"""
Initialise PiCamera2, configure le flux et démarre le pipeline libcamera.
"""
try:
from picamera2 import Picamera2 # Import local : disponible uniquement sur Pi
except ImportError as exc:
raise CaptureError(
"picamera2 introuvable — installez-le avec : "
"sudo apt install python3-picamera2"
) from exc
try:
self._picam2 = Picamera2(self._camera_index)
# Choix de la configuration selon le mode sélectionné
if self._use_video_config:
config = self._picam2.create_video_configuration(
main={"size": (self._width, self._height), "format": "RGB888"},
)
logger.debug("Configuration VideoConfiguration sélectionnée")
else:
config = self._picam2.create_still_configuration(
main={"size": (self._width, self._height), "format": "RGB888"},
)
logger.debug("Configuration StillConfiguration sélectionnée")
self._picam2.configure(config)
self._picam2.start()
logger.info(
"PiCamera2 ouverte : index=%d résolution=%dx%d mode=%s",
self._camera_index,
self._width,
self._height,
"video" if self._use_video_config else "still",
)
except Exception as exc:
# Nettoyage en cas d'échec partiel d'initialisation
if self._picam2 is not None:
try:
self._picam2.close()
except Exception: # noqa: BLE001
pass
self._picam2 = None
raise CaptureError(f"Impossible d'ouvrir PiCamera2 : {exc}") from exc
def close(self) -> None:
"""Arrête le pipeline libcamera et libère les ressources."""
if self._picam2 is not None:
try:
self._picam2.stop()
self._picam2.close()
logger.info("PiCamera2 fermée (index=%d)", self._camera_index)
except Exception as exc: # noqa: BLE001
logger.warning("Erreur lors de la fermeture de PiCamera2 : %s", exc)
finally:
self._picam2 = None
def capture_frame(self) -> bytes:
"""
Capture une image depuis le flux libcamera et l'encode en JPEG.
Stratégie : capture_array() → tableau NumPy RGB → encodage Pillow.
capture_file() ne supporte pas le paramètre quality ; on encode
manuellement pour contrôler le taux de compression.
:return: Données JPEG brutes
:raises CaptureError: Si la capture ou l'encodage échoue
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
try:
from PIL import Image
# Récupération du tableau RGB depuis le flux libcamera
arr = self._picam2.capture_array("main") # shape (H, W, 3) uint8
# Encodage manuel en JPEG avec la qualité configurée
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="JPEG", quality=self._jpeg_quality)
return buf.getvalue()
except Exception as exc:
raise CaptureError(f"Échec de capture PiCamera2 : {exc}") from exc
def is_available(self) -> bool:
"""Retourne True si le pipeline libcamera est démarré."""
return self._picam2 is not None
# ------------------------------------------------------------------
# Méthodes spécifiques à PiCamera2
# ------------------------------------------------------------------
def capture_high_res(self, width: int, height: int) -> bytes:
"""
Capture une image haute résolution hors flux principal (photo ponctuelle).
Utile pour déclencher une capture pleine résolution pendant un flux 5 fps.
Utilise capture_request() + make_array() + encodage Pillow.
:param width: Largeur souhaitée en pixels (indicatif, dépend de la config active)
:param height: Hauteur souhaitée en pixels
:return: Données JPEG brutes
:raises CaptureError: Si PiCamera2 n'est pas initialisée
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
try:
from PIL import Image
# Capture d'une requête unique depuis le flux actif
request = self._picam2.capture_request()
arr = request.make_array("main") # shape (H, W, 3) uint8 RGB
request.release()
buf = io.BytesIO()
Image.fromarray(arr).save(buf, format="JPEG", quality=self._jpeg_quality)
return buf.getvalue()
except Exception as exc:
raise CaptureError(f"Échec de capture haute résolution : {exc}") from exc
def set_controls(self, **kwargs) -> None:
"""
Applique des contrôles libcamera directement (exposition, gain, balance des blancs…).
Exemple ::
cam.set_controls(ExposureTime=10000, AnalogueGain=2.0)
:param kwargs: Contrôles libcamera valides pour le module connecté
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
self._picam2.set_controls(kwargs)
logger.debug("Contrôles appliqués : %s", kwargs)
def get_camera_properties(self) -> dict:
"""
Retourne les métadonnées du module caméra détecté.
:return: Dictionnaire des propriétés (modèle, résolution max, etc.)
"""
if self._picam2 is None:
raise CaptureError("PiCamera2 n'est pas initialisée")
return self._picam2.camera_properties
@property
def jpeg_quality(self) -> int:
"""Qualité JPEG [0-100]."""
return self._jpeg_quality
@jpeg_quality.setter
def jpeg_quality(self, value: int) -> None:
if not 0 <= value <= 100:
raise ValueError("La qualité JPEG doit être comprise entre 0 et 100")
self._jpeg_quality = value
@property
def resolution(self) -> tuple[int, int]:
"""Résolution de capture configurée (largeur, hauteur)."""
return (self._width, self._height)
+122
View File
@@ -0,0 +1,122 @@
'''
Created on 21 août 2024
@author: denis
'''
import logging
import asyncio
from typing import Union
from abc import ABC
from reduct import Client, Bucket, BucketSettings #, QuotaType
from reduct.time import unix_timestamp_from_any, TIME_PRECISION #, unix_timestamp_to_datetime
from datetime import datetime, timedelta
from django.utils import timezone
from django.conf import settings
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ReductStoreBase(ABC):
def __init__(self, url, api_token, name, quota_type=None, quota_size=1000_000_000):
self.bucket_name = name
self.client = Client(url, api_token=api_token)
self.quota_type = quota_type
self.quota_size = quota_size
self.bucket: Bucket = asyncio.run(self.create_bucket())
logger.info(f"==== {url} token:{api_token}")
async def create_bucket(self):
settings = BucketSettings(
quota_type=self.quota_type,
quota_size=self.quota_size,
exist_ok=True,
)
return await self.client.create_bucket(self.bucket_name, settings, exist_ok=True)
async def change_bucket(self, **settings):
new_settings = BucketSettings(**settings)
await self.bucket.set_settings(new_settings)
async def remove_bucket(self):
await self.bucket.remove()
async def write(self, entry_name, data, timestamp=None, content_type=None, labels=None):
await self.bucket.write(entry_name, data, timestamp=timestamp, content_type=content_type, labels=labels)
def query(self, entry_name, start, stop, ttl=None, when=None):
return self.bucket.query(entry_name, start=start, stop=stop, ttl=ttl, when=when)
async def remove_query(self, entry_name, start, stop, when=None):
return await self.bucket.remove_query(entry_name, start=start, stop=stop, when=when)
async def read(self, entry_name, timestamp, head=False):
async with self.bucket.read(entry_name, timestamp=timestamp, head=head) as record:
return await record.read_all()
async def record_content(self, entry_name, timestamp, head=False):
async with self.bucket.read(entry_name, timestamp=timestamp, head=head) as record:
content = await record.read_all()
return record, content
class ReductStore(ReductStoreBase):
def __init__(self, name):
super().__init__(settings.REDUCTSTORE_URL, settings.REDUCTSTORE_TOKEN, name=name)
async def old_last_dates(client_db, entry_name='uuid'):
oldest, latest = 0, 0
infos = await client_db.bucket.get_entry_list()
for info in infos:
if info.name == entry_name:
oldest, latest = info.oldest_record, info.latest_record
break
return oldest, latest
async def date_posterior_to(client_db, uuid: str, post: Union[int, str]):
last = None
oldest, latest = await old_last_dates(client_db, uuid)
if oldest and post:
dtpost = timezone.now() - timedelta(seconds=int(post))
ts = unix_timestamp_from_any(dtpost)
last = ts if ts < latest else None
return oldest, last
async def dates_filter(client_db, uuid: str, begin: Union[int, datetime, float, str],
end: Union[int, datetime, float, str] = None, duration: int = 0):
oldest, latest = await old_last_dates(client_db, uuid)
ts_from, ts_to = 0, 0
if latest:
ts_from = unix_timestamp_from_any(begin) if begin else oldest
if ts_from < oldest:
ts_from = oldest
if end is None:
if not duration:
ts_to = latest
else:
ts_to = ts_from + (duration * TIME_PRECISION)
if ts_to > latest:
ts_to = latest
else:
ts_to = unix_timestamp_from_any(end) if end else latest
if ts_to > latest:
ts_to = latest
return ts_from, ts_to if ts_from<ts_to else latest
+138
View File
@@ -0,0 +1,138 @@
'''
Created on 3 févr. 2026
@author: denis
'''
from django.conf import settings
# myapp/system_stats.py
import threading
import time
import os
import psutil
# intervale de mise à jour (secondes)
REFRESH_INTERVAL = 5
RAMDISK = "/mnt/ramdisk"
_cache = {
"shm": [],
"cpu_info": {},
"memory_info": {},
"disk_info": {},
"ramdisk_info": {},
"updated_at": None
}
_lock = threading.Lock()
_timer = None
def _collect_once():
data = {}
# shm: liste /dev/shm si disponible
try:
path = "/dev/shm"
data["shm"] = os.listdir(path) if os.path.exists(path) and os.path.isdir(path) else []
except Exception as e:
data["shm_error"] = str(e)
# cpu_info
try:
cpu_times = psutil.cpu_times_percent(interval=None, percpu=False)._asdict()
data["cpu_info"] = {
"cpu_count": psutil.cpu_count(logical=True),
"cpu_count_physical": psutil.cpu_count(logical=False),
"cpu_percent": psutil.cpu_percent(interval=None),
"cpu_times_percent": cpu_times
}
except Exception as e:
data["cpu_info_error"] = str(e)
# memory_info
try:
vm = psutil.virtual_memory()._asdict()
sm = psutil.swap_memory()._asdict()
data["memory_info"] = {"virtual_memory": vm, "swap_memory": sm}
except Exception as e:
data["memory_info_error"] = str(e)
# disk_info (root and partitions)
# ex: if mountpoint == "/ramdisk" and fstype=="tmpfs" then usage.percent, usage.free, etc ...
try:
usage_root = psutil.disk_usage("/")._asdict()
parts = []
for p in psutil.disk_partitions(all=False):
try:
du = psutil.disk_usage(p.mountpoint)._asdict()
except Exception:
du = {}
parts.append({"device": p.device, "mountpoint": p.mountpoint, "fstype": p.fstype, "usage": du})
data["disk_info"] = {"root": usage_root, "partitions": parts}
except Exception as e:
data["disk_info_error"] = str(e)
# ramdisk
# ex: if mountpoint == "/ramdisk" and fstype=="tmpfs" then usage.percent, usage.free, etc ...
try:
for part in psutil.disk_partitions(all=True):
if part.mountpoint == RAMDISK and part.fstype.lower() == "tmpfs":
usage = psutil.disk_usage(part.mountpoint)
data["ramdisk_info"] = {
"percent": usage.percent,
"mount": part.mountpoint,
"device": part.device,
"fstype": part.fstype,
"total": usage.total,
"used": usage.used,
"free": usage.free,
}
except Exception as e:
data["ramdisk_info_error"] = str(e)
data["updated_at"] = time.time()
return data
def _update_cache():
global _timer
try:
new = _collect_once()
with _lock:
_cache.update(new)
finally:
# reprogrammer
_timer = threading.Timer(REFRESH_INTERVAL, _update_cache)
_timer.daemon = True
_timer.start()
def start_background_updater(interval_seconds: int = None):
global REFRESH_INTERVAL, _timer
if interval_seconds:
REFRESH_INTERVAL = interval_seconds
if _timer is not None:
return
# première collecte synchronisée
with _lock:
_cache.update(_collect_once())
_timer = threading.Timer(REFRESH_INTERVAL, _update_cache)
_timer.daemon = True
_timer.start()
def stop_background_updater():
global _timer
if _timer is not None:
_timer.cancel()
_timer = None
def get_cached_stats():
with _lock:
# retourner copie pour sécurité
return dict(_cache)
+261
View File
@@ -0,0 +1,261 @@
'''
Created on 20 avr. 2022
@author: denis
'''
import yaml
import time
import importlib
from datetime import datetime
import string, secrets
import uuid
from threading import Event, Thread
from urllib.parse import urlsplit
import asyncio
# sysutils.py
import os
import mmap
import fcntl
import psutil
SHM_DIR = "/dev/shm"
def open_shm(name: str, size: int, create=True):
path = os.path.join(SHM_DIR, name)
flags = os.O_RDWR | (os.O_CREAT if create else 0)
fd = os.open(path, flags)
try:
if create:
os.ftruncate(fd, size)
mm = mmap.mmap(fd, size)
finally:
os.close(fd)
return mm, path
def read_shm(name: str, size: int):
mm, path = open_shm(name, size, create=False) # @UnusedVariable
try:
mm.seek(0)
data = mm.read(size)
return data.rstrip(b"\0")
finally:
mm.close()
def write_shm(name: str, size: int, data: bytes):
mm, path = open_shm(name, size, create=True)
fd = os.open(path, os.O_RDWR)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
try:
mm.seek(0)
mm.write(data.ljust(size, b"\0")[:size])
mm.flush()
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
finally:
mm.close()
os.close(fd)
def get_tmpfs_info(mount_point="/ramdisk"):
def sizeof(n):
for unit in ['B','KB','MB','GB','TB']:
if n < 1024:
return f"{n:.1f}{unit}"
n /= 1024
return f"{n:.1f}PB"
usage = None
for part in psutil.disk_partitions(all=True):
if part.mountpoint == mount_point and part.fstype.lower() == "tmpfs":
usage = psutil.disk_usage(part.mountpoint)
print(f"Mount: {part.mountpoint}")
print(f" Device: {part.device}")
print(f" Fstype: {part.fstype}")
print(f" Total: {usage.total} bytes ({sizeof(usage.total)})")
print(f" Used: {usage.used} bytes ({sizeof(usage.used)})")
print(f" Free: {usage.free} bytes ({sizeof(usage.free)})")
print(f" Percent used: {usage.percent}%")
break
return {
"percent": usage.percent,
"mount": part.mountpoint,
"device": part.device,
"fstype": part.fstype,
"total": usage.total,
"used": usage.used,
"free": usage.free,
}
def get_cpu_info():
# cpu percent par coeur et moyennes load
return {
"cpu_percent_per_cpu": psutil.cpu_percent(interval=0.5, percpu=True),
"cpu_percent_total": psutil.cpu_percent(interval=None),
"load_avg": os.getloadavg(), # (1,5,15)
"cpu_count": psutil.cpu_count(logical=True),
}
def get_memory_info():
vm = psutil.virtual_memory()
sm = psutil.swap_memory()
return {
"total": vm.total,
"available": vm.available,
"used": vm.used,
"free": vm.free,
"percent": vm.percent,
"swap_total": sm.total,
"swap_used": sm.used,
"swap_free": sm.free,
"swap_percent": sm.percent,
}
def get_disk_info(path="/"):
du = psutil.disk_usage(path)
return {
"path": path,
"total": du.total,
"used": du.used,
"free": du.free,
"percent": du.percent,
}
def extract_host_port_path(url, default_port=None):
"""
Retoure (host, port, path) où:
- host: string (IP ou hostname) ou None
- port: int ou None (utilise default_port si fourni et aucun port explicite)
- path: string (chemin + query + fragment si présents), ou '' si absent
"""
parts = urlsplit(url if '://' in url else '//' + url, scheme='')
host = parts.hostname
port = parts.port or default_port
# Reconstruire path complet: path + ('?' + query) + ('#' + fragment)
path = parts.path or ''
if parts.query:
path += '?' + parts.query
if parts.fragment:
path += '#' + parts.fragment
return host, port, path
def image_path(imagefile):
image_path = imagefile.path
pdir = os.path.dirname(image_path)
os.makedirs(pdir, exist_ok=True)
return str(image_path)
def serialize_datetime(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError("Type not serializable")
def start_async():
loop = asyncio.new_event_loop()
Thread(target=loop.run_forever, daemon=True).start()
return loop
def stop_async(loop):
loop.call_soon_threadsafe(loop.stop)
def submit_async(loop, awaitable):
return asyncio.run_coroutine_threadsafe(awaitable, loop)
def to_choice(d):
choices = []
for k, v in d.items():
choices.append((k, v))
return choices
def get_instance_class(module):
modulename, classname = module.rsplit(".", 1)
return getattr(importlib.import_module(modulename), classname)
def wait_for(timeout):
Event().wait(timeout)
def yaml_load(f):
with open(f, 'r') as stream:
return yaml.safe_load(stream)
return {}
def yaml_save(f, context):
with open(f, 'w') as stream:
yaml.dump(context, stream, default_flow_style = False)
def get_uuid():
return str(hex(uuid.getnode()))[2:]
def millis():
return round(time.time() * 1000)
def now():
return datetime.now()
def ts_now():
# float second
return now().timestamp()
def ts_now_s():
return int(ts_now())
def ts_now_ms():
return int(ts_now()*1000)
def ts_now_us():
return int(ts_now()*1000000)
def random_num(n=16):
alphabet = string.digits
return ''.join(secrets.choice(alphabet) for i in range(n)) # @UnusedVariable
def random_chars(n=6):
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for i in range(n)) # @UnusedVariable
def get_apikey(n=32):
chars = 'abcdefgh01234ijklABCD4567EFGHIJKLmnopqrstuvwxyz0123456789MNOPQRS789TUVWXYZ'
return ''.join(secrets.choice(chars) for i in range(n)) # @UnusedVariable
def gen_keywords(s):
c = s.replace(',', ' ').replace('+', ' ')
return [ w.strip() for w in c.split(' ') if w]
def gen_device_uuid(n=19):
return hex(int(random_num(n)))[2:]
def get_device_uuid(n=19):
return f'0x{gen_device_uuid(n)}'
+152
View File
@@ -0,0 +1,152 @@
"""
Implémentation de la capture vidéo pour webcam via OpenCV (cv2).
Dépendance : opencv-python (pip install opencv-python)
OpenCV (cv2) avec import local pour éviter une dépendance globale
Résolution configurable, qualité JPEG réglable à chaud, accès V4L2 par index
get_resolution() pour lire la résolution effective appliquée par le pilote
"""
import os
os.environ['OPENCV_LOG_LEVEL']="0"
os.environ['OPENCV_FFMPEG_LOGLEVEL']="0"
import cv2
import logging
from typing import Optional
from .capture_interface import CaptureError, VideoCaptureInterface
logger = logging.getLogger(__name__)
class WebcamCapture(VideoCaptureInterface):
"""
Capture JPEG depuis une webcam USB/intégrée via OpenCV.
Exemple d'utilisation ::
cam = WebcamCapture(device_index=0, fps=5)
cam.set_frame_callback(lambda data, ts: print(f"{ts}: {len(data)} octets"))
cam.start()
time.sleep(10)
cam.stop()
"""
def __init__(
self,
device_index: int = 0,
fps: float = VideoCaptureInterface.DEFAULT_FPS,
jpeg_quality: int = 85,
width: Optional[int] = None,
height: Optional[int] = None,
):
"""
:param device_index: Index du périphérique V4L2 (0 = première webcam)
:param fps: Cadence cible en images par seconde
:param jpeg_quality: Qualité de compression JPEG [0-100]
:param width: Largeur souhaitée (None = valeur par défaut du pilote)
:param height: Hauteur souhaitée (None = valeur par défaut du pilote)
"""
super().__init__(fps=fps)
self._device_index: int = device_index
self._jpeg_quality: int = jpeg_quality
self._width: Optional[int] = width
self._height: Optional[int] = height
self._cap = None # Instance cv2.VideoCapture
# ------------------------------------------------------------------
# Implémentation des méthodes abstraites
# ------------------------------------------------------------------
def open(self) -> None:
"""Ouvre le flux V4L2 via OpenCV et configure la résolution."""
self._cap = cv2.VideoCapture(self._device_index)
if not self._cap.isOpened():
raise CaptureError(
f"Impossible d'ouvrir la webcam (index={self._device_index})"
)
# Application de la résolution demandée
if self._width:
self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, self._width)
if self._height:
self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self._height)
# Lecture de la résolution effectivement appliquée par le pilote
actual_w = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_h = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
logger.info(
"Webcam ouverte : index=%d résolution=%dx%d",
self._device_index, actual_w, actual_h,
)
def close(self) -> None:
"""Libère le flux OpenCV."""
if self._cap and self._cap.isOpened():
self._cap.release()
logger.info("Webcam fermée (index=%d)", self._device_index)
self._cap = None
def capture_frame(self) -> bytes:
"""
Lit une trame brute depuis OpenCV et l'encode en JPEG.
:return: Données JPEG brutes
:raises CaptureError: Si la lecture ou l'encodage échoue
"""
#import cv2
#import numpy as np # noqa: F401 — utilisé implicitement par cv2
if not self._cap or not self._cap.isOpened():
raise CaptureError("La webcam n'est pas ouverte")
ret, frame = self._cap.read()
if not ret or frame is None:
raise CaptureError("Échec de lecture de la trame")
# Encodage BGR → JPEG avec la qualité configurée
encode_params = [cv2.IMWRITE_JPEG_QUALITY, self._jpeg_quality]
success, buffer = cv2.imencode(".jpg", frame, encode_params)
if not success:
raise CaptureError("Échec d'encodage JPEG")
return buffer.tobytes()
def is_available(self) -> bool:
"""Retourne True si le flux OpenCV est ouvert et prêt."""
return self._cap is not None and self._cap.isOpened()
# ------------------------------------------------------------------
# Accesseurs spécifiques à la webcam
# ------------------------------------------------------------------
@property
def device_index(self) -> int:
"""Index du périphérique V4L2."""
return self._device_index
@property
def jpeg_quality(self) -> int:
"""Qualité JPEG [0-100]."""
return self._jpeg_quality
@jpeg_quality.setter
def jpeg_quality(self, value: int) -> None:
if not 0 <= value <= 100:
raise ValueError("La qualité JPEG doit être comprise entre 0 et 100")
self._jpeg_quality = value
def get_resolution(self) -> Optional[tuple[int, int]]:
"""
Retourne la résolution effective du flux.
:return: Tuple (largeur, hauteur) ou None si la webcam est fermée
"""
if not self.is_available():
return None
w = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
return (w, h)
+64
View File
@@ -0,0 +1,64 @@
from django.contrib import admin
from django.db.models import Q
from . import models
class WellAdmin(admin.ModelAdmin):
model = models.Well
list_display = ('name', 'author',)
class ConfigurationAdmin(admin.ModelAdmin):
list_display = ('name', 'author', 'use_rpicam', 'video_width_capture', 'video_height_capture', 'video_frame_rate', 'active',)
class MultiWellAdmin(admin.ModelAdmin):
list_filter = ('author',)
list_display = ('label', 'position', 'author', 'order', 'xbase', 'ybase', 'duration', 'feed', 'active',)
class ObservationMultiWellDetailInline(admin.TabularInline):
model = models.ObservationMultiWellDetail
extra = 0
class ObservationAdmin(admin.ModelAdmin):
inlines = (ObservationMultiWellDetailInline,)
list_filter = ('sessionobservation__session', 'author', )
list_display = ('title', 'author', 'multiwell', 'created', 'started', 'finished')
readonly_fields = ('created', 'started', 'finished', )
class SessionObservationInlineAdmin(admin.TabularInline):
model = models.SessionObservation
fk_name = 'session'
extra = 0
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "observation":
obj_id = request.resolver_match.kwargs.get("object_id")
qs = models.Observation.objects.filter(sessionobservation__isnull=True)
if obj_id:
qs = models.Observation.objects.filter(
Q(sessionobservation__isnull=True) |
Q(sessionobservation__session_id=obj_id)
)
kwargs["queryset"] = qs.distinct()
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class SessionAdmin(admin.ModelAdmin):
list_filter = ('author',)
inlines = (SessionObservationInlineAdmin, )
list_display = ('name', 'author', 'created', 'finished', 'active', 'expected_export', 'expected_scanning', )
readonly_fields = (
'created',
'finished',
'export_status',
'export_task',
'export_exported_at',
'scanning_status',
'scanning_task',
'scanning_finished_at'
)
admin.site.register(models.Configuration, ConfigurationAdmin)
admin.site.register(models.Well, WellAdmin)
admin.site.register(models.MultiWell, MultiWellAdmin)
admin.site.register(models.Observation, ObservationAdmin)
admin.site.register(models.Session, SessionAdmin)
+8
View File
@@ -0,0 +1,8 @@
from django.apps import AppConfig
class ScannerConfig(AppConfig):
name = 'scanner'
def ready(self):
import scanner.models # noqa — active les signaux post_save/post_delete
+62
View File
@@ -0,0 +1,62 @@
#
#from django.utils.translation import gettext_lazy as _
#from channels.layers import get_channel_layer
#from django.conf import settings
#import asyncio
#from asgiref.sync import sync_to_async
import json, logging
from channels.generic.websocket import AsyncWebsocketConsumer
from .process import redisDB
logger = logging.getLogger(__name__)
class ScannerConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.this_group = f"scanner_proc"
await self.channel_layer.group_add(self.this_group, self.channel_name)
await self.accept()
logger.info(f"==== connected to {self.this_group}")
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.this_group, self.channel_name)
logger.info( f"==== Disconnect from {self.this_group}")
async def scanner_message(self, event):
await self.send(text_data=json.dumps(event["text"]))
## Receive message from WebSocket
async def receive(self, text_data):
data = json.loads(text_data)
msg_type = data.get("type")
if msg_type in ["scanner", "calibrate"]:
redisDB.publish(self.this_group, json.dumps(data))
async def replay_message(self, event):
await self.send(text_data=json.dumps(event["text"]))
class ReplayConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.this_group = f"replay_proc"
await self.channel_layer.group_add(self.this_group, self.channel_name)
await self.accept()
logger.info(f"==== connected to {self.this_group}")
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.this_group, self.channel_name)
logger.info( f"==== Disconnect from {self.this_group}")
async def replay_message(self, event):
await self.send(text_data=json.dumps(event["text"]))
## Receive message from WebSocket
async def receive(self, text_data):
data = json.loads(text_data)
msg_type = data.get("type")
if msg_type == "replay":
redisDB.publish(self.this_group, json.dumps(data))
+604
View File
@@ -0,0 +1,604 @@
# Tâches d'exportation pour les vidéos de caméra
import zipfile
from celery.utils.log import get_task_logger
import shutil
import os, sys
import posix_ipc
import mmap
import cv2
import numpy as np
from django.http import JsonResponse, HttpResponse
from django.conf import settings
from reduct.time import unix_timestamp_to_iso
from .process import CameraRecordManager, cameraDB
logger = get_task_logger(__name__)
def progress_bar(iteration, total, prefix='', suffix='', length=30, fill=''):
percent = ("{0:.1f}").format(100 * (iteration / float(total)))
filled_length = int(length * iteration // total)
bar = fill * filled_length + '-' * (length - filled_length)
sys.stdout.write(f'\r{prefix} |{bar}| {percent}% {suffix}')
sys.stdout.flush()
def delete_file_later(path):
try:
if os.path.exists(path):
os.remove(path)
except Exception as e:
logger.error(f"[cleanup] error deleting {path}: {e}")
raise
async def remove_video_by_uuid(uuid, start_ts=None, end_ts=None, when=None):
record_manager = CameraRecordManager(cameraDB)
await record_manager.remove(uuid, start_ts, end_ts)
async def remove_video(uuid, start_ts, end_ts, when=None):
try:
await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
return JsonResponse({'state': 'ok'}, status=200)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
try:
record_manager = CameraRecordManager(cameraDB)
total_size = await record_manager.size(uuid, start_ts, end_ts)
# segment de mémoire partagée pour stocker les frames
shm_size = int(total_size * 1.5)
shm_name = f"/video_frames_{uuid}"
try:
shm = posix_ipc.SharedMemory(shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size)
except posix_ipc.ExistentialError:
existing = posix_ipc.SharedMemory(shm_name, flags=0)
existing.close_fd()
try:
existing.unlink()
except posix_ipc.ExistentialError:
pass
shm = posix_ipc.SharedMemory(shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size)
mm = mmap.mmap(shm.fd, shm_size)
queries = record_manager.query(uuid, start_ts, end_ts)
offset = 0
frame_sizes = []
total = 0
async for record in queries:
frame_bytes = await record.read_all()
frame_size = len(frame_bytes)
mm[offset:offset + frame_size] = frame_bytes
frame_sizes.append(frame_size)
offset += frame_size
total += 1
if not frame_sizes:
return JsonResponse({'error': 'Aucune frame trouvée'}, status=404)
video_path = os.path.join(settings.MEDIA_ROOT, f"output.{opencv_video_type}")
fourcc = cv2.VideoWriter_fourcc(* opencv_fourcc_format)
# Lit les frames depuis la mémoire partagée
current_offset = 0
i = 0
for size in frame_sizes:
frame_bytes = mm[current_offset:current_offset + size]
nparr = np.frombuffer(frame_bytes, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if 'video' not in locals():
height, width, _ = frame.shape
video = cv2.VideoWriter(video_path, fourcc, frame_rate, (width, height))
video.write(frame)
current_offset += size
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
i+=1
video.release()
# Nettoie la mémoire partagée
shm.unlink()
# Vérifier que le fichier existe
if not os.path.exists(video_path):
logger.error(f"Fichier non créé: {video_path}")
return JsonResponse({'error': 'Erreur création vidéo'}, status=500)
# Lit le fichier vidéo généré
with open(video_path, 'rb') as f:
video_bytes = f.read()
# Retourne la vidéo en réponse
response = HttpResponse(video_bytes, content_type='video/mp4')
response['Content-Disposition'] = f'attachment; filename="{video_path}"'
response['Content-Length'] = os.path.getsize(video_path)
# Supprime le fichier temporaire
os.remove(video_path)
return response
except Exception as e:
logger.error(f"shm_download_video: {e}")
return JsonResponse({'error': str(e)}, status=500)
# ─────────────────────────────────────────────
##
# ─────────────────────────────────────────────
def remote_mount_available(mount_point: str = "/mnt/exports_cam") -> bool:
"""
Vérifie que le point de montage Samba est actif et accessible en écriture.
"""
return os.path.ismount(mount_point) and os.access(mount_point, os.W_OK)
def _copy_to_destinations(source_path: str, filename: str) -> dict:
"""
Copie le fichier exporté vers les destinations configurées.
Retourne un dict avec les chemins effectivement écrits.
"""
results = {"local": None, "remote": None}
for dest in settings.EXPORT_DESTINATIONS:
if dest == "local":
# Déjà sur place, rien à copier
results["local"] = source_path
elif dest == "remote":
remote_path = os.path.join(settings.EXPORT_REMOTE_DIR, filename)
try:
if not remote_mount_available(settings.EXPORT_REMOTE_DIR):
logger.warning("Partage Samba non disponible, copie ignorée")
results["remote_error"] = "Montage indisponible"
continue
# Copie locale vers le point de montage Samba
shutil.copy2(source_path, remote_path)
results["remote"] = remote_path
logger.info("Copie distante OK : %s", remote_path)
except OSError as exc:
# Le partage est peut-être absent (machine Windows éteinte)
logger.error("Copie distante échouée [%s] : %s", remote_path, exc)
results["remote_error"] = str(exc)
return results
# ─────────────────────────────────────────────
# Tâche 1 : Export des frames en ZIP d'images
# ─────────────────────────────────────────────
async def export_images_zip(
uuid: str,
start_ts: float | None = None,
end_ts: float | None = None,
jpeg_quality: int = 85,
max_zip_size_mb: int = 0,
max_image_width: int = 0,
max_image_height: int = 0,
):
"""
Exporte les frames d'une caméra sous forme d'archive ZIP contenant des JPEG.
:param uuid: Identifiant de la caméra
:param start_ts: Timestamp de début (epoch secondes)
:param end_ts: Timestamp de fin (epoch secondes)
:param max_zip_size_mb: Taille maximale du ZIP en Mo (0 = illimité)
:param jpeg_quality: Qualité JPEG 1-100 (défaut 85)
:param max_image_width: Redimensionnement max largeur en px (0 = non redimensionné)
:param max_image_height: Redimensionnement max hauteur en px (0 = non redimensionné)
:return: Chemin du fichier ZIP généré + rapport JSON
"""
shm = None
mm = None
shm_name = f"/img_frames_{uuid}"
try:
# --- Chargement des frames en mémoire partagée ---
record_manager = CameraRecordManager(cameraDB)
total_size = await record_manager.size(uuid, start_ts, end_ts)
shm_size = int(total_size * 1.5)
try:
shm = posix_ipc.SharedMemory(
shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
)
except posix_ipc.ExistentialError:
existing = posix_ipc.SharedMemory(shm_name, flags=0)
existing.close_fd()
try:
existing.unlink()
except posix_ipc.ExistentialError:
pass
shm = posix_ipc.SharedMemory(
shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
)
mm = mmap.mmap(shm.fd, shm_size)
queries = record_manager.query(uuid, start_ts, end_ts)
if not start_ts:
start_ts = record_manager.oldest_ts
if not end_ts:
end_ts = record_manager.latest_ts
offset = 0
frame_sizes = []
ts = []
total = 0
async for record in queries:
ts.append(record.timestamp)
frame_bytes = await record.read_all()
frame_size = len(frame_bytes)
mm[offset:offset + frame_size] = frame_bytes
frame_sizes.append(frame_size)
offset += frame_size
total += 1
if not frame_sizes:
return {"status": "error", "message": "Aucune frame trouvée"}
# --- Génération du ZIP ---
max_zip_bytes = max_zip_size_mb * 1024 * 1024 if max_zip_size_mb > 0 else 0
ts_s = unix_timestamp_to_iso(start_ts)
zip_filename = f"{uuid}_{ts_s}.zip"
zip_path = os.path.join(settings.EXPORTS_LOCAL_PATH, 'images', zip_filename)
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
skipped = 0
written = 0
current_offset = 0
encode_params = [cv2.IMWRITE_JPEG_QUALITY, jpeg_quality]
i = 0
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for idx, size in enumerate(frame_sizes):
# Vérification de la taille du ZIP
if max_zip_bytes and os.path.getsize(zip_path) >= max_zip_bytes:
skipped += len(frame_sizes) - idx
logger.warning(
"export_images_zip: limite %d Mo atteinte, %d frames ignorées",
max_zip_size_mb,
len(frame_sizes) - idx,
)
break
frame_bytes = mm[current_offset:current_offset + size]
nparr = np.frombuffer(frame_bytes, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if frame is None:
current_offset += size
skipped += 1
continue
# Redimensionnement optionnel
if max_image_width > 0 or max_image_height > 0:
frame = _resize_frame(frame, max_image_width, max_image_height)
# Encodage JPEG en mémoire puis ajout au ZIP
ok, buf = cv2.imencode(".jpg", frame, encode_params)
if ok:
#zf.writestr(f"frame_{idx:06d}.jpg", buf.tobytes())
ts_iso = unix_timestamp_to_iso(ts[idx])
#logger.info(f"export_images_zip: adding frame {idx} with timestamp {ts_iso} to ZIP")
zf.writestr(f"{uuid}_{ts_iso}.jpg", buf.tobytes())
written += 1
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
i+=1
current_offset += size
## Copie vers les destinations (local + Samba)
#destinations = _copy_to_destinations(zip_path, zip_filename)
return {
"status": "success",
"zip_path": zip_path,
"frames_written": written,
"frames_skipped": skipped,
"jpeg_quality": jpeg_quality,
#"destinations": destinations,
}
except Exception as exc:
logger.error("export_images_zip [%s]: %s", uuid, exc, exc_info=True)
return {"status": "error", "message": str(exc)}
finally:
if mm:
mm.close()
if shm:
try:
shm.unlink()
except posix_ipc.ExistentialError:
pass
# ─────────────────────────────────────────────
# Tâche 2 : Export des frames en vidéo MP4
# ─────────────────────────────────────────────
#@shared_task(bind=True)
async def export_video_mp4(
uuid: str,
start_ts: float | None = None,
end_ts: float | None = None,
frame_rate: int = 5,
opencv_fourcc_format='mp4v',
opencv_video_type='mp4',
max_video_size_mb: int = 0,
max_width: int = 0,
max_height: int = 0,
):
"""
Exporte les frames d'une caméra en fichier MP4 via OpenCV.
:param uuid: Identifiant de la caméra
:param start_ts: Timestamp de début (epoch secondes)
:param end_ts: Timestamp de fin (epoch secondes)
:param frame_rate: Images par seconde (défaut 5)
:param max_video_size_mb: Taille maximale du MP4 en Mo (0 = illimité)
:param max_width: Redimensionnement max largeur en px (0 = non redimensionné)
:param max_height: Redimensionnement max hauteur en px (0 = non redimensionné)
:return: Chemin du fichier MP4 généré + rapport JSON
"""
shm = None
mm = None
video = None
shm_name = f"/vid_frames_{uuid}"
try:
# --- Chargement des frames en mémoire partagée ---
record_manager = CameraRecordManager(cameraDB)
total_size = await record_manager.size(uuid, start_ts, end_ts)
shm_size = int(total_size * 1.5)
try:
shm = posix_ipc.SharedMemory(
shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
)
except posix_ipc.ExistentialError:
existing = posix_ipc.SharedMemory(shm_name, flags=0)
existing.close_fd()
try:
existing.unlink()
except posix_ipc.ExistentialError:
pass
shm = posix_ipc.SharedMemory(
shm_name, posix_ipc.O_CREAT | posix_ipc.O_EXCL, size=shm_size
)
mm = mmap.mmap(shm.fd, shm_size)
queries = record_manager.query(uuid, start_ts, end_ts)
if not start_ts:
start_ts = record_manager.oldest_ts
if not end_ts:
end_ts = record_manager.latest_ts
offset = 0
frame_sizes = []
total = 0
async for record in queries:
frame_bytes = await record.read_all()
frame_size = len(frame_bytes)
mm[offset:offset + frame_size] = frame_bytes
frame_sizes.append(frame_size)
offset += frame_size
total +=1
if not frame_sizes:
return {"status": "error", "message": "Aucune frame trouvée"}
# --- Génération du MP4 ---
max_video_bytes = max_video_size_mb * 1024 * 1024 if max_video_size_mb > 0 else 0
ts_s = unix_timestamp_to_iso(start_ts)
video_path = os.path.join(
settings.EXPORTS_LOCAL_PATH, 'videos', f"{uuid}_{ts_s}.{opencv_video_type}"
)
os.makedirs(os.path.dirname(video_path), exist_ok=True)
fourcc = cv2.VideoWriter_fourcc(*opencv_fourcc_format)
skipped = 0
written = 0
current_offset = 0
i=0
for idx, size in enumerate(frame_sizes):
# Vérification de la taille du MP4 en cours
if (
max_video_bytes
and video is not None
and os.path.exists(video_path)
and os.path.getsize(video_path) >= max_video_bytes
):
skipped += len(frame_sizes) - idx
logger.warning(
"export_video_mp4: limite %d Mo atteinte, %d frames ignorées",
max_video_size_mb,
len(frame_sizes) - idx,
)
break
frame_bytes = mm[current_offset:current_offset + size]
nparr = np.frombuffer(frame_bytes, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if frame is None:
current_offset += size
skipped += 1
continue
# Redimensionnement optionnel
if max_width > 0 or max_height > 0:
frame = _resize_frame(frame, max_width, max_height)
# Initialisation du VideoWriter sur la première frame valide
if video is None:
h, w, _ = frame.shape
video = cv2.VideoWriter(video_path, fourcc, frame_rate, (w, h))
video.write(frame)
written += 1
current_offset += size
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
i+=1
if video:
video.release()
if not os.path.exists(video_path):
return {"status": "error", "message": f"Fichier {opencv_video_type} non créé"}
## Copie vers les destinations (local + Samba)
#filename = os.path.basename(video_path)
#destinations = _copy_to_destinations(video_path, filename)
return {
"status": "success",
"video_path": video_path,
"frames_written": written,
"frames_skipped": skipped,
"frame_rate": frame_rate,
"file_size_mb": round(os.path.getsize(video_path) / 1024 / 1024, 2),
#"destinations": destinations,
}
except Exception as exc:
logger.error("export_video_mp4 [%s]: %s", uuid, exc, exc_info=True)
return {"status": "error", "message": str(exc)}
finally:
if mm:
mm.close()
if shm:
try:
shm.unlink()
except posix_ipc.ExistentialError:
pass
# ─────────────────────────────────────────────
# Utilitaire commun
# ─────────────────────────────────────────────
def _resize_frame(
frame: np.ndarray, max_width: int, max_height: int
) -> np.ndarray:
"""
Redimensionne une frame en conservant le ratio si un max est dépassé.
max_width ou max_height à 0 signifie sans contrainte sur cet axe.
"""
h, w = frame.shape[:2]
scale = 1.0
if max_width > 0 and w > max_width:
scale = min(scale, max_width / w)
if max_height > 0 and h > max_height:
scale = min(scale, max_height / h)
if scale < 1.0:
new_w = int(w * scale)
new_h = int(h * scale)
frame = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
return frame
# tasks/export_tasks.py
from celery import shared_task, group
from django.utils import timezone
import logging
logger = logging.getLogger(__name__)
@shared_task(bind=True)
def run_session_exports(self, session_id: str):
"""
Point d'entrée déclenché par django_celery_beat.
Lance en parallèle l'export images et l'export vidéo de la session.
"""
from cameras.models import ExportSession
try:
session = ExportSession.objects.get(session_id=session_id)
except ExportSession.DoesNotExist:
logger.error("run_session_exports: session %s introuvable", session_id)
return {"status": "error", "message": "Session introuvable"}
session.status = ExportSession.Status.RUNNING
session.save(update_fields=["status"])
try:
# Lancement en parallèle avec group Celery
job = group(
export_all_images.s(session_id),
export_all_videos.s(session_id),
).apply_async()
results = job.get(timeout=7200) # 2h max pour les deux
session.status = ExportSession.Status.DONE
session.exported_at = timezone.now()
session.save(update_fields=["status", "exported_at"])
return {"status": "success", "results": results}
except Exception as exc:
session.status = ExportSession.Status.ERROR
session.save(update_fields=["status"])
logger.error("run_session_exports [%s]: %s", session_id, exc, exc_info=True)
return {"status": "error", "message": str(exc)}
@shared_task(bind=True)
def export_all_images(self, session_id: str):
"""
Export ZIP de toutes les images de la session.
"""
from cameras.models import ExportSession
session = ExportSession.objects.get(session_id=session_id)
return export_images_zip(
session.camera_uuid,
session.start_ts,
session.end_ts,
max_zip_size_mb = session.max_zip_size_mb,
jpeg_quality = session.jpeg_quality,
max_image_width = session.max_image_width,
max_image_height = session.max_image_height,
)
@shared_task(bind=True)
def export_all_videos(self, session_id: str):
"""
Export MP4 de toutes les vidéos de la session.
"""
from cameras.models import ExportSession
session = ExportSession.objects.get(session_id=session_id)
return export_video_mp4(
session.camera_uuid,
session.start_ts,
session.end_ts,
frame_rate = session.frame_rate,
max_video_size_mb = session.max_video_size_mb,
max_width = session.max_width,
max_height = session.max_height,
)
+308
View File
@@ -0,0 +1,308 @@
# Django models for test tube scanner application
# Created on 10/04/2024
# denis@linuxtarn.org
from django.utils.translation import gettext_lazy as _
import uuid
import json
from django_celery_beat.models import PeriodicTask, ClockedSchedule
from django.dispatch import receiver
from django.db.models.signals import post_save, post_delete
from django.utils import timezone
from django.db import models
from django.contrib.auth.models import User
# Multi-well positions on the table for calibration and observation
MULTIWELL_POSITION = [
('HG', _("Haut gauche")),
('HD', _("Haut droit")),
('BG', _("Bas gauche")),
('BD', _("Bas droit")),
('BM', _("Bas milieu")),
('HM', _("Haut milieu")),
]
FOURCC_FORMAT = [
('mp4v', _("MP4")),
('XVID', _("XVID")),
]
VIDEO_TYPE = [
('mp4', _("MP4")),
('avi', _("AVI")),
]
class Configuration(models.Model):
name = models.CharField(_("Nom de la Configuration"), help_text=_("Nom de la configuration"), max_length=100, null=True, blank=False, default=_("Configuration par défaut"))
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
# Dashboard configuration
sidebar_width = models.CharField(_("Barre latérale"), help_text=_("Largeur barre latérale (css)"), max_length=32, null=True, blank=False, default="350px")
default_grid_columns = models.PositiveSmallIntegerField(_("Colonnes de la grille par défaut"), help_text=_("Nombre de colonnes de la grille par défaut"), blank=False, default=3)
# opencv
opencv_fourcc_format = models.CharField(_("Fourcc"), help_text=_('Opencv fourcc format'), max_length=8, choices=FOURCC_FORMAT, null=True, blank=False, default='mp4v')
opencv_video_type = models.CharField(_("Video type"), help_text=_('Opencv video type'), max_length=8, choices=VIDEO_TYPE, null=True, blank=False, default='mp4')
# Grbl configuration
grbl_xmax = models.FloatField(_("Grbl Xmax"), help_text=_("CNC Grbl Xmax en mm"), blank=False, default=350.0)
grbl_ymax = models.FloatField(_("Grbl Ymax"), help_text=_("CNC Grbl Ymax en mm"), blank=False, default=250.0)
# camera configuration
use_rpicam = models.BooleanField(_("Utiliser rpicam"), help_text=_("Par défaaut. Sinon USB webcam"), default=True)
webcam_device_index = models.PositiveSmallIntegerField(_("Index de la webcam"), help_text=_("Index de la webcam (0, 1, ...) si présente"), default=2)
image_quality = models.PositiveSmallIntegerField(_("Qualité JPEG"), help_text=_("Qualité JPEG (1-100) pour les images exportées"), default=90)
video_jpeg_quality = models.PositiveSmallIntegerField(_("Qualité JPEG pour les vidéos"), help_text=_("Qualité JPEG (1-100) pour les images extraites des vidéos"), default=90)
video_frame_rate = models.FloatField(_("Fréquence vidéos (fps)"), help_text=_("Fréquence d'extraction des images des vidéos (images par seconde)"), default=5.0)
video_width_capture = models.PositiveSmallIntegerField(_("Largeur de capture vidéo"), help_text=_("Largeur de capture vidéo en pixels"), default=1280)
video_height_capture = models.PositiveSmallIntegerField(_("Hauteur de capture vidéo"), help_text=_("Hauteur de capture vidéo en pixels"), default=720)
# Calibration
calibration_crop_radius = models.PositiveSmallIntegerField(_("Rayon de découpe pour la calibration"), help_text=_("Rayon en pixels pour découper les images de calibration en px"), default=150)
calibration_default_multiwell = models.CharField(_("Multi-puits de calibration par défaut"), help_text=_("Position du multi-puits de calibration par défaut"), max_length=8, choices=MULTIWELL_POSITION, default='HG')
calibration_default_feed = models.PositiveIntegerField(_("Vitesse de calibration"), help_text=_("Vitesse de déplacement pour la calibration en mm/mn"), default=1000)
calibration_default_step = models.FloatField(_("Pas de calibration"), help_text=_("Pas de déplacement pour la calibration en mm"), default=1.0)
active = models.BooleanField(_("Actif"), default=False)
class Meta:
ordering = ['id', ]
verbose_name = _("Configuration")
verbose_name_plural = verbose_name
def __str__(self):
return f'{self.name}'
class Well(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
name = models.CharField(_("Nom"), help_text=_("Nom du puit: Ai..Di"), unique=True, max_length=4, null=True, blank=True)
class Meta:
ordering = ['name', ]
verbose_name = _("Puit")
verbose_name_plural = _("Puits")
def __str__(self):
return f'{self.name}'
class MultiWell(models.Model):
label = models.CharField(_("Label"), help_text=_("Label du multi-puit"), max_length=100, null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
position = models.CharField(_("Position"), help_text=_('Position du multi-puits sur la table'), unique=True, max_length=8, choices=MULTIWELL_POSITION, null=True, blank=False)
cols = models.PositiveSmallIntegerField(_("Colonnes"), help_text=_('Nombre de colonnes'), blank=False, default=6)
rows = models.PositiveSmallIntegerField(_("Lignes"), help_text=_('Nombre de lignes'), blank=False, default=4)
row_def = models.CharField(_("Définition"), help_text=_('Définition des lignes'), max_length=16, null=True, blank=False, default="A,B,C,D")
row_order = models.CharField(_("Ordre"), help_text=_('Ordre de lecture en serpentin'), max_length=16, null=True, blank=False, default="D,C,B,A")
order = models.PositiveSmallIntegerField(_("Ordre"), help_text=_('Ordre de lecture du multi-puit'), blank=False, default=0)
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée du film en secondes'), blank=False, default=120)
xbase = models.FloatField(_("Origine X"), help_text=_('Base origine X en mm'), blank=False, default=50.0)
ybase = models.FloatField(_("Origine Y"), help_text=_('Base origine Y en mm'), blank=False, default=50.0)
dx = models.FloatField(_("Pas X"), help_text=_('Pas ou interval sur X en mm'), blank=False, default=19.5)
dy = models.FloatField(_("Pas Y"), help_text=_('Pas ou interval sur Y en mm'), blank=False, default=19.5)
feed = models.PositiveIntegerField(_("Vitesse"), help_text=_('Vitesse déplacement en mm/mn '), blank=False, default=1000)
active = models.BooleanField(_("Active"), default=True)
def config(self):
return dict(
cols=self.cols,
rows=self.rows,
row_def=self.row_def,
row_order=self.row_order,
dx=self.dx,
dy=self.dy,
duration=self.duration,
feed=self.feed,
xbase=self.xbase,
ybase=self.ybase,
)
return {}
@classmethod
def config_by_position(cls, position):
qs = MultiWell.objects.filter(position__exact=position).values()
if qs:
return dict(qs[0])
return {}
@classmethod
def by_position(cls, position):
return MultiWell.objects.filter(position__exact=position).first()
@classmethod
def all(cls):
return MultiWell.objects.filter(active=True).all()
class Meta:
ordering = ['order', ]
verbose_name = _("Multi-puits")
verbose_name_plural = _("Multi-puits")
def __str__(self):
return f'{self.position}: {self.label}'
class Observation(models.Model):
title = models.CharField(_("Titre de l'observation"), max_length=100, null=True, blank=False)
comment = models.TextField(_("Commentaires"), help_text=_("Descriptions de l'observations"), null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
created = models.DateTimeField(_("Date de création"), default=timezone.now)
started = models.DateTimeField (_("Date de début"), null=True, blank=True)
finished = models.DateTimeField (_("Date de fin"), null=True, blank=True)
class Meta:
ordering = ['-created', ]
verbose_name = _("Observation")
verbose_name_plural = _("Observations")
def __str__(self):
return f'{self.title}: {self.created} {self.multiwell.order}'
class ObservationMultiWellDetail(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
observation = models.ForeignKey(Observation, on_delete=models.CASCADE, related_name="multiwell_details" , null=True, blank=True)
well = models.ForeignKey(Well, verbose_name="Puit", on_delete=models.CASCADE, related_name="observation_details", null=True, blank=True )
detail = models.CharField("Détail", max_length=255)
comment = models.TextField("Commentaire", blank=True)
class Meta:
ordering = ['observation', 'well__name']
unique_together = ["observation", "well"]
verbose_name = _("Observation multi-puits détail")
verbose_name_plural = _("Observations multi-puits détails")
def __str__(self):
return f"{self.observation.title} - {self.well} - {self.detail}"
class Session(models.Model):
class Status(models.TextChoices):
PENDING = "pending", _("En attente")
RUNNING = "running", _("En cours")
DONE = "done", _("Terminé")
ERROR = "error", _("Erreur")
name = models.CharField(_("Nom de la session"), help_text=_("Session d'observations. 4 Multi-puits maximum"), max_length=100, null=True, blank=False)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
active = models.BooleanField(_("Active"), default=True)
expected_export = models.DateTimeField(_("Date d'exportation"), help_text=_("Date d'exportation prévue"), null=True, blank=True)
expected_scanning = models.DateTimeField(_("Date du balayage"), help_text=_("Date du balayage prévue"), null=True, blank=True)
created = models.DateTimeField(_("Date de création"), default=timezone.now)
finished = models.DateTimeField (_("Date de fin"), null=True, blank=True)
export_status = models.CharField(_("Status exportation"), max_length=16, choices=Status.choices, default=Status.PENDING)
export_task = models.OneToOneField(
"django_celery_beat.PeriodicTask",
verbose_name=_("Export médias"),
help_text=_("Programmation de l'exportation des vidéos et images"),
null=True, blank=True, on_delete=models.SET_NULL, related_name="export_session")
export_exported_at = models.DateTimeField(_("Exportation terminée à"), null=True, blank=True)
scanning_status = models.CharField(_("Status scanning"), max_length=16, choices=Status.choices, default=Status.PENDING)
scanning_task = models.OneToOneField(
"django_celery_beat.PeriodicTask",
verbose_name=_("Lancer le balayage"),
help_text=_("Programmation du lancement du balayage"),
null=True, blank=True, on_delete=models.SET_NULL, related_name="scanning_session")
scanning_finished_at = models.DateTimeField(_("Balayage terminé à"), null=True, blank=True)
class Meta:
ordering = ['-created', ]
verbose_name = _("Session d'observation")
verbose_name_plural = _("Sessions d'observation")
def __str__(self):
state = _("Terminée") if not self.active else _("Active")
return f'{self.name}: {state}'
@receiver(post_save, sender=Session)
def create_periodic_task(sender, instance, created, **kwargs):
"""
Crée automatiquement une PeriodicTask à la création d'une session.
La tâche est one-shot : elle se désactive après exécution (one_off=True).
"""
if instance.expected_export:
try:
clocked, _ = ClockedSchedule.objects.get_or_create(clocked_time=instance.expected_export)
export_task = PeriodicTask.objects.create(
name = f"export_session_{instance.id}",
task = "scanner.tasks.run_session_exports",
clocked = clocked,
one_off = True, # se désactive après la première exécution
enabled = True,
last_run_at = None, # force Celery Beat à ne pas la considérer déjà exécutée
start_time = None, # pas de contrainte de démarrage
kwargs = json.dumps({ # paramètres passés à la tâche
"session_id": str(instance.id),
}),
description = f"Export expected at {instance.expected_export}{instance.name}",
)
# Sauvegarde sans re-déclencher le signal
Session.objects.filter(pk=instance.pk).update(export_task=export_task)
except:
pass
if instance.expected_export:
try:
clocked, _ = ClockedSchedule.objects.get_or_create(clocked_time=instance.expected_scanning)
scanning_task = PeriodicTask.objects.create(
name = f"scanning_session_{instance.id}",
task = "scanner.tasks.run_scanning",
clocked = clocked,
one_off = True, # se désactive après la première exécution
enabled = True,
last_run_at = None, # force Celery Beat à ne pas la considérer déjà exécutée
start_time = None, # pas de contrainte de démarrage
kwargs = json.dumps({ # paramètres passés à la tâche
"session_id": str(instance.id),
}),
description = f"Scanning expected at {instance.expected_scanning}{instance.name}",
)
# Sauvegarde sans re-déclencher le signal
Session.objects.filter(pk=instance.pk).update(scanning_task=scanning_task)
except:
pass
@receiver(post_delete, sender=Session)
def delete_periodic_task(sender, instance, **kwargs):
"""
Supprime la PeriodicTask associée quand la session est supprimée.
"""
if instance.export_task:
instance.export_task.delete()
if instance.scanning_task:
instance.scanning_task.delete()
class SessionObservation(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.SET_NULL, null=True, blank=True)
observation = models.ForeignKey(Observation, verbose_name=_("Observation"), on_delete=models.SET_NULL, null=True, blank=True)
@classmethod
def observation_by_session(cls, session_id, active=True):
return [ ss.observation for ss in SessionObservation.objects.filter(session__id=session_id, session__active=active).order_by('observation__multiwell__order') ]
@classmethod
def uuid_from_session(cls, sid):
observations = [ss.observation for ss in SessionObservation.objects.filter(session__id=sid, session__active=False)]
uuid_list = []
for obs in observations:
row_def = obs.multiwell.row_def.split(',')
for row in range(obs.multiwell.rows):
for col in range(obs.multiwell.cols):
uuid = f'{sid}-{obs.multiwell.position}-{row_def[row]}{col+1}'
uuid_list.append(uuid)
return uuid_list
class Meta:
ordering = ['session',]
unique_together = ["session", "observation"]
verbose_name = _("Session observations")
verbose_name_plural = _("Sessions observations")
def __str__(self):
return f'{self.session.name}'
+812
View File
@@ -0,0 +1,812 @@
#
# process.py
import os
os.environ['OPENCV_LOG_LEVEL']="0"
os.environ['OPENCV_FFMPEG_LOGLEVEL']="0"
import cv2
from django.utils.translation import gettext_lazy as _
from datetime import datetime
import time, asyncio, bisect
import json, base64
from threading import Thread, Event, Lock
from queue import Queue
from asgiref.sync import async_to_sync #, sync_to_async
from channels.layers import get_channel_layer
from django.utils import timezone
from django.conf import settings
from celery import Task
from celery.exceptions import Ignore
from celery.utils.log import get_task_logger
from redis import Redis
from dataclasses import dataclass
from modules import reductstore, grbl, utils
## camera devices
from modules.circular_crop import CircularCrop, CropStrategy
from . import models
@dataclass
class ProcTag:
play: bool = True
record: bool = False
uuid: str = None
session: int = 0
logger = get_task_logger(__name__)
redisDB = Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=0, decode_responses=True)
cameraDB = reductstore.ReductStore(name='camera')
class CameraRecordManager():
def __init__(self, clienDB):
self.clienDB = clienDB
self.is_image = False
self.oldest_ts = None
self.latest_ts = None
async def size(self, uuid, start_ts, end_ts):
try:
queries = self.query(uuid, start_ts, end_ts)
total_size = 0
record_number = 0
latest = None
async for record in queries:
if not record_number:
self.oldest_ts = record.timestamp
frame_bytes = await record.read_all()
total_size += len(frame_bytes)
record_number += 1
latest = record.timestamp
self.latest_ts = latest
return total_size
except:
return None
def black_jpg(self):
frame = cv2.imread(settings.MEDIA_ROOT / 'images' / 'black-screen.jpg', cv2.IMREAD_UNCHANGED)
_, frame = cv2.imencode('.jpg', frame)
black_jpg = frame.tobytes()
return f'data:image/jpeg;base64,{base64.b64encode(black_jpg).decode()}'
def set_filters(self, session=None, test=None):
filters = []
if session:
filters.append({"&session": { "$eq": session} })
if test==True:
filters.append({"&test": { "$contains": "True"} })
when = {"$and": filters}
return when
async def record_content(self, query):
record = await anext(query)
content = await record.read_all()
return record, content
def query(self, uuid, start=None, stop=None, filters=None):
try:
return self.clienDB.query(uuid, start, stop, when=filters, ttl=3600)
except Exception as e:
logger.error(f"CameraRecordManager query: {e}")
def first_image(self, uuid, start=None, stop=None, filters=None):
try:
query = self.query(uuid, start, stop, filters=filters)
record, content = async_to_sync(self.record_content)(query)
self.is_image = True
return f'data:image/jpeg;base64,{base64.b64encode(content).decode()}', record.timestamp
except Exception as e: # @UnusedVariable
pass
#logger.error(f"CameraRecordManager first_image: {e}")
self.is_image = False
return self.black_jpg(), start
def write(self, uuid, frame, labels, ts=None):
try:
if ts is None:
ts = timezone.now()
async_to_sync(self.clienDB.write)(
uuid,
frame,
timestamp=ts,
labels=labels,
content_type='application/octet-stream',
)
except Exception as e:
logger.error(f"CameraRecordManager write: {e}")
async def remove_uuid(self, uuid, start=None, stop=None, when=None):
try:
await self.clienDB.remove_query(uuid, start, stop, when=when)
except Exception as e:
logger.error(f"CameraRecordManager remove: {e}")
def remove(self, uuid, start=None, stop=None, when=None):
asyncio.run(self.remove_uuid(uuid, start, stop, when=when))
class MultiWellManager:
def __init__(self, position, feed=None, step=None, proc=None):
self.set_multiwell(position)
self._feed = feed
self._step = step
self.proc = proc
self.scanner = None
def set_multiwell(self, position):
self._position = position
self.well = models.MultiWell.by_position(position)
self._xbase = self.well.xbase
self._ybase = self.well.ybase
self._dx = self.well.dx
self._dy = self.well.dy
def _start_test(self):
self.scanner.start()
def _start(self, machine, session, observations):
xynext = []
for obs in observations:
xynext.append((obs.multiwell.xbase, obs.multiwell.ybase))
xynext.append((0, 0))
pos = 1
self.proc.session = session.id
started = timezone.now()
for obs in observations:
conf = obs.multiwell.config()
self.scanner = grbl.GridScanner(machine, proc=self.proc, **conf)
obs.started = timezone.now()
obs.save()
xnext, ynext = xynext[pos]
pos +=1
self.scanner.start(xnext=xnext, ynext=ynext, position=obs.multiwell.position)
obs.finished = timezone.now()
obs.save()
session.finished = timezone.now()
session.active = False
session.save()
logger.info(f"==== Session {session.name} terminée à {session.finished} après {session.finished - started} secondes.")
def scan_test(self, machine, duration=5.0):
conf = self.well.config()
conf['duration'] = duration
conf['feed'] = self.feed
conf['xnext'] = self._xbase
conf['ynext'] = self._ybase
self.proc.session = 0
self.scanner = grbl.GridScanner(machine, proc=self.proc, **conf)
Thread(target=self._start_test, daemon=True).start()
def scan(self, machine, sid):
try:
session = models.Session.objects.get(pk=sid)
observations = models.SessionObservation.observation_by_session(sid)
Thread(target=self._start, args=(machine, session, observations, ), daemon=True).start()
except Exception as e:
print("MultiWellManager::scan error", e)
def halt(self):
if self.scanner:
self.scanner.halt()
@property
def position(self):
return self._position
@position.setter
def position(self, value):
self._position = value
@property
def step(self):
return self._step
@step.setter
def step(self, value):
self._step = value
@property
def feed(self):
return self._feed
@feed.setter
def feed(self, value):
self._feed = value
@property
def xbase(self):
return self._xbase
@xbase.setter
def xbase(self, value):
self._xbase = value
@property
def ybase(self):
return self._ybase
@ybase.setter
def ybase(self, value):
self._ybase = value
@property
def dx(self):
return self._dx
@dx.setter
def dx(self, value):
self._dx = value
@property
def dy(self):
return self._dy
@dy.setter
def dy(self, value):
self._dy = value
def set_xy_step(self):
models.MultiWell.objects.filter(position__exact=self.position).update(dx=self.dx, dy=self.dy)
def set_position(self, machine):
x, y = machine.get_mpos()
machine.wait_for(2.0)
models.MultiWell.objects.filter(position__exact=self.position).update(xbase=x, ybase=y)
self._xbase, self._ybase = x, y
class ScannerProcess(Task):
'''
video_quality = settings.VIDEO_JPG_QUALITY
image_quality = settings.IMAGE_JPG_QUALITY
video_fps = settings.VIDEO_FPS
video_width = settings.VIDEO_WIDTH
video_height = settings.VIDEO_HEIGHT
crop_radius = settings.CALIBRATION_CROP_RADIUS
default_multiwell = settings.CALIBRATION_DEFAULT_MULTIWELL
default_feed = settings.CALIBRATION_DEFAULT_FEED
default_step = settings.CALIBRATION_DEFAULT_STEP'''
def __init__(self):
super().__init__()
self.channel_layer = get_channel_layer()
self.group = f'scanner_proc'
self.stop_event = Event()
self.cam = None
self.grbl = None
self.crop = None
self.multiwel = None
self.conf = None
self.record_queue = Queue()
self.proc = ProcTag()
self.manager = None
self.recordDB = CameraRecordManager(cameraDB)
def __call__(self, *args, **kwargs):
return self.start(*args, **kwargs)
def set_crop_radius(self, radius):
return CircularCrop(radius=radius, strategy=CropStrategy.CROP_JPEG, jpeg_quality=self.image_quality)
def start(self, *args, **kwargs):
try:
self.conf = models.Configuration.objects.filter(active=True).first()
self.video_quality = self.conf.video_jpeg_quality
self.image_quality = self.conf.image_quality
self.video_fps = self.conf.video_frame_rate
self.video_width = self.conf.video_width_capture
self.video_height = self.conf.video_height_capture
self.crop_radius = self.conf.calibration_crop_radius
self.default_multiwell = self.conf.calibration_default_multiwell
self.default_feed = self.conf.calibration_default_feed
self.default_step = self.conf.calibration_default_step
self.video_jpg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), self.video_quality]
self.image_jpg_quality = [int(cv2.IMWRITE_JPEG_QUALITY), self.image_quality]
self.grbl_xmax = self.conf.grbl_xmax
self.grbl_ymax = self.conf.grbl_ymax
#self.crop = CircularCrop(radius=self.crop_radius, strategy=CropStrategy.CROP_JPEG, jpeg_quality=self.image_quality)
self.crop = self.set_crop_radius(self.crop_radius)
if not self.conf.use_rpicam:
from modules.webcam_capture import WebcamCapture
self.cam = WebcamCapture(
device_index=self.conf.webcam_device_index,
fps=self.video_fps,
width=self.video_width,
height=self.video_height,
jpeg_quality=self.video_quality,
)
else:
from modules.picamera2_capture import PiCamera2Capture
self.cam = PiCamera2Capture(
fps=self.video_fps,
width=self.video_width,
height=self.video_height,
jpeg_quality=self.video_quality,
)
self.cam.set_frame_callback(self._on_frame)
self.cam.set_median(False)
self.cam.set_circular_crop(None)
self.stop_event.clear()
self.start_services()
except Exception as e:
logger.error(f"Scanner started error: {e}")
raise Ignore()
def stop(self):
try:
info = 'Scanner stopped'
self._send(state='stop', msg=info)
self.stop_event.set()
self.cam.stop()
logger.info(info)
Event().wait(1.0)
finally:
self.stop_event.set()
def start_services(self):
Thread(target=self._listen_to_redis, daemon=True).start()
Thread(target=self._recording, daemon=True).start()
self.cam.start()
def _send(self, **payload):
async_to_sync(self.channel_layer.group_send)(
self.group, {
"type": 'scanner.message',
"text": payload
}
)
def _display(self, **msg):
if self.grbl:
self._send(**msg)
def _on_frame(self, jpeg_bytes: bytes, ts: datetime) -> None:
if self.proc.record:
# record images
self.record_queue.put((self.proc.uuid, ts, jpeg_bytes))
if self.proc.play:
# play image
self._send(ts=ts.timestamp(), jpeg=base64.b64encode(jpeg_bytes).decode(), )
def _recording(self):
logger.info(f"Scanner {self.group}: start recorder")
while not self.stop_event.is_set():
try:
(uuid, ts, frame) = self.record_queue.get()
labels = dict(fps=self.video_fps, session=self.proc.session)
self.recordDB.write(uuid, frame, labels, ts=ts)
self.record_queue.task_done()
except Exception as e:
logger.error(f'recorder: {e}')
def _init_grbl(self, feed=1000):
self.grbl = grbl.GRBLController(
send_callback=self._display,
x_max=self.conf.grbl_xmax,
y_max=self.conf.grbl_ymax
)
self.grbl.go_origin(feed=feed)
self.grbl.wait_for(2.0)
def _listen_to_redis(self):
try:
logger.info(f"==== Scanner {self.group}: listen via redisDB")
pubsub = redisDB.pubsub()
pubsub.subscribe(self.group)
self._init_grbl()
self.manager = MultiWellManager(
self.default_multiwell,
feed=self.default_feed,
step=self.default_step,
proc=self.proc
)
for message in pubsub.listen():
try:
#logger.info(f"{message}")
if self.stop_event.is_set():
break
cmd = json.loads(str(message.get('data')))
logger.info(f"{cmd}")
if not isinstance(cmd, dict):
continue
self._send(state=cmd["type"], msg=f"Cmd: {cmd.get('topic')} {cmd.get('value', '')}")
if cmd["type"]=="scanner":
topic = cmd.get("topic")
if topic == 'init':
self.cam.set_circular_crop(self.crop)
self.cam.set_median(is_median=False)
self.grbl.go_origin(feed=self.manager.feed)
elif topic == 'scan':
sid = cmd.get("session", '0')
if sid == "0":
self._send(state='error', msg=str(_('La session est nulle!...')))
else:
self.cam.set_median(is_median=False)
self.manager.scan(self.grbl, sid)
elif cmd["type"]=="calibrate":
topic = cmd.get("topic")
value = cmd.get("value")
if topic == 'init':
self.manager.feed = int(cmd.get("feed", self.default_feed))
self.manager.step = float(cmd.get("step", self.default_step))
position = cmd.get("position", self.default_multiwell)
if self.manager.position != position:
self.manager.set_multiwell(position)
self.cam.set_circular_crop(None)
self.cam.set_median(is_median=False)
elif topic == 'up':
self.grbl.move_relative(dy=self.manager.step, feed=self.manager.feed)
elif topic == 'down':
self.grbl.move_relative(dy=-self.manager.step, feed=self.manager.feed)
elif topic == 'right':
self.grbl.move_relative(dx=self.manager.step, feed=self.manager.feed)
elif topic == 'left':
self.grbl.move_relative(dx=-self.manager.step, feed=self.manager.feed)
elif topic == 'median':
self.cam.set_median(is_median=value)
elif topic == 'crop':
self.cam.set_circular_crop(self.crop) if value else self.cam.set_circular_crop(None)
continue
elif topic == 'crop_radius':
self.conf.calibration_crop_radius=int(value)
self.crop = self.set_crop_radius(self.conf.calibration_crop_radius)
self.conf.save()
self.cam.set_circular_crop(self.crop)
continue
elif topic == 'position':
self.manager.set_multiwell(value)
elif topic == 'step':
self.manager.step = float(value)
elif topic == 'feed':
self.manager.feed = int(value)
elif topic == 'goto_0':
self.grbl.go_origin(feed=self.manager.feed)
elif topic == 'goto_xy':
self.grbl.move_to(self.manager.xbase, self.manager.ybase, feed=self.manager.feed)
elif topic == 'xy_base':
self.manager.set_position(self.grbl)
elif topic == 'dx':
self.manager.dx = float(value)
elif topic == 'dy':
self.manager.dy = float(value)
elif topic == 'xy_step':
self.manager.set_xy_step()
elif topic == 'test':
self.manager.scan_test(self.grbl)
continue
elif topic == 'halt':
self.manager.halt()
continue
self._send(
xbase=self.manager.xbase,
ybase=self.manager.ybase,
x=self.grbl.x,
y=self.grbl.y,
xy=True,
dxy=True,
dx=self.manager.dx,
dy=self.manager.dy
)
except Exception as e:
logger.error(f'scanner listen_to_redis: {e}')
finally:
pubsub.unsubscribe()
pubsub.close()
#=================================================================
#
# REPLAY Buffer glissant
#
# temps réel replay →
# |---- préchargé ----|---- en lecture ----|---- à venir ----|
# -2s t +3s
# max_seconds:
# 3s → faible latence, faible RAM
# 10s → seek ultra fluide
#=================================================================
class ReplayBuffer:
def __init__(self, max_seconds=5.0):
self.max_seconds = max_seconds
self.frames = {} # ts → bytes
self.timestamps = [] # triée
self.lock = Lock()
def push(self, ts, frame):
with self.lock:
if ts in self.frames:
return
bisect.insort(self.timestamps, ts)
self.frames[ts] = frame
self._cleanup(ts)
def get_nearest(self, ts_us: int):
try:
with self.lock:
if not self.timestamps:
return None
idx = bisect.bisect_right(self.timestamps, ts_us) - 1
if idx < 0:
idx = 0
nearest_ts = self.timestamps[idx]
return nearest_ts, self.frames[nearest_ts]
except Exception as e: # @UnusedVariable
pass
#logger.error(f"{e}")
return None
def clear(self):
with self.lock:
self.frames.clear()
self.timestamps.clear()
def _cleanup(self, current_ts):
# supprime les frames trop anciennes
limit = current_ts - self.max_seconds
while self.timestamps and self.timestamps[0] < limit:
ts = self.timestamps.pop(0)
del self.frames[ts]
class ReplayClock:
def __init__(self, uuid, start_ts, stop_ts, fps=5.0, speed=1.0):
self.uuid = uuid
self.start_ts = start_ts
self.stop_ts = stop_ts
self.ts = start_ts
self.fps = fps
self.speed = speed
self.paused = False
self._seek_ts = None
self._last_tick = time.monotonic()
self.lock = Lock()
self.delta = self.stop_ts - self.start_ts
def tick(self):
with self.lock:
now = time.monotonic()
dt_sec = now - self._last_tick
self._last_tick = now
delta_us = int(dt_sec * 1_000_000 * self.speed)
self.ts += delta_us
if self.ts >= self.stop_ts:
self.paused = True
return None
return self.ts
def sleep_duration(self) -> float:
with self.lock:
frame_us = int(1_000_000 / self.fps)
return max((frame_us / self.speed) / 1_000_000, 0.001)
def play(self):
with self.lock:
self.paused = False
def pause(self):
with self.lock:
self.paused = True
def stop(self):
with self.lock:
self.paused = True
self.ts = self.start_ts
return self.ts
def set_speed(self, speed):
with self.lock:
self.speed = max(0.1, speed)
def seek(self, k):
with self.lock:
self._seek_ts = int( self.start_ts + (k * self.delta) )
def consume_seek(self):
with self.lock:
ts = self._seek_ts
self._seek_ts = None
return ts
return None
def progress(self, ts: int) -> float:
ptx = (ts - self.start_ts) / self.delta
return round( max(0.0, min(1.0, ptx)), 6)
class ReplayProcess(Task):
def __init__(self, latency=5.0):
super().__init__()
self.channel_layer = get_channel_layer()
self.latency = latency
self.group = f'replay_proc'
self.recordDB = cameraDB
self.stop_event = Event()
self.clock = None
self.query = None
self.running = asyncio.Event()
def __call__(self, uuid, *args, **kwargs):
return self.start(*args, **kwargs)
def start(self, *args, **kwargs):
try:
self.stop_event.clear()
Thread(target=self._listen_to_redis, daemon=True).start()
except Exception as e:
logger.error(f"Replay error: {e}")
raise Ignore()
def stop(self):
self.stop_event.set()
logger.info(f"==== ReplayProcess stopped.")
def _listen_to_redis(self):
try:
loop = None
logger.info(f"==== ReplayProcess {self.group}: listen via redisDB")
pubsub = redisDB.pubsub()
pubsub.subscribe(self.group)
for message in pubsub.listen():
try:
if self.stop_event.is_set():
break
cmd = json.loads(str(message.get('data')))
logger.info(f"{cmd}")
if not isinstance(cmd, dict):
continue
if cmd["type"] == "replay":
action = cmd["action"]
if action in ["init", "play"]:
uuid = cmd.get("uuid")
start_ts, stop_ts = int(cmd.get('dt_start')), int(cmd.get('dt_stop'))
fps, speed = float(cmd.get('fps', 5.0)), int(cmd.get('speed'))
self.clock = ReplayClock(uuid, start_ts, stop_ts, fps, speed)
if action == "init":
if loop:
utils.stop_async(loop)
loop = None
elif action == "play":
if not loop:
loop = utils.start_async()
utils.submit_async(loop, self._replay())
self.clock.play()
elif action == 'stop':
self.running.set()
if loop:
utils.stop_async(loop)
loop = None
ts = self.clock.stop()
async_to_sync(self._send_message)('video-reset', dt_start=ts, percent=0.0)
elif action == 'pause':
self.clock.pause()
elif action == 'speed':
self.clock.set_speed(cmd.get("value"))
elif action == 'seek':
k = float(cmd.get("value"))
self.clock.seek(k)
except Exception as e:
logger.error(f'ReplayProcess::listen_to_redis: {e}')
finally:
self.running.set()
utils.stop_async(loop)
pubsub.unsubscribe()
pubsub.close()
async def _send(self, payload):
await self.channel_layer.group_send(self.group, {"type": 'replay.message', "text": payload })
async def _send_message(self, motif, **msg):
payload = {
'uuid': self.clock.uuid,
'motif': motif,
**msg
}
await self._send(payload)
async def _send_frame(self, ts, jpg_bytes):
payload = {
'uuid': self.clock.uuid,
"ts": ts,
"progress": self.clock.progress(ts),
"jpeg": base64.b64encode(jpg_bytes).decode()
}
await self._send(payload)
def _create_query(self, clock):
return self.recordDB.query(clock.uuid, start=clock.ts, stop=clock.stop_ts )
async def _replay(self):
try:
self.running.clear()
query = self._create_query(self.clock)
self.buffer = ReplayBuffer(max_seconds=self.latency)
while not self.running.is_set():
try:
# ---- seek ? ----
seek_ts = self.clock.consume_seek()
if seek_ts is not None:
self.clock.ts = seek_ts
query = self._create_query(self.clock)
await asyncio.sleep(0.01)
continue
# ---- pause ----
if self.clock.paused:
await asyncio.sleep(0.5)
continue
# ---- frame ----
record = await anext(query)
frame = await record.read_all()
self.buffer.push(record.timestamp, frame)
if record.timestamp < self.clock.ts + self.buffer.max_seconds:
continue
# ---- get frame ----
nearest = self.buffer.get_nearest(self.clock.ts)
if nearest is None:
await asyncio.sleep(0.01)
continue
# ---- emit jpg ----
frame_ts, jpg = nearest
await self._send_frame(frame_ts, jpg)
# ---- avance temps ----
self.clock.tick()
await asyncio.sleep(self.clock.sleep_duration())
except StopAsyncIteration:
self.clock.pause()
self.buffer.clear()
except Exception as e:
logger.error(f'_replay loop: {e}')
await asyncio.sleep(0.5)
except Exception as e:
logger.error(f"_replay: {e}")
+13
View File
@@ -0,0 +1,13 @@
#
# routing.py
from django.urls import re_path
from django.conf import settings
from . import consumers
urla = settings.SCANNER_WEBSOCKET_ROUTE
urlb = settings.REPLAY_WEBSOCKET_ROUTE
websocket_urlpatterns = [
re_path(urla, consumers.ScannerConsumer.as_asgi()),
re_path(urlb, consumers.ReplayConsumer.as_asgi()),
]
@@ -0,0 +1,34 @@
.container {
height: 100%;
padding: 0.25em;
display: grid;
grid-template-columns: 180px 1fr 150px;
grid-template-rows: 64px 1fr;
gap: 1em 1em;
grid-auto-flow: row;
grid-template-areas:
"header header header"
"move scanner scan"
}
.header {
align-self: stretch;
grid-area: header;
}
.scanner {
justify-self: center;
align-self: center;
grid-area: scanner;
}
.scan {
padding: 0 0.25em;
align-self: start;
grid-area: scan;
}
.move {
align-self: start;
grid-area: move;
}
@@ -0,0 +1,28 @@
.multiwell_cards {
display: grid;
/*grid-auto-columns: 1fr;
grid-auto-rows: 1fr;*/
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: 0.1em;
grid-template-areas:
". ."
". ."
". .";
}
button.multiwell {
padding: 0.15em ;
}
#image-grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns, 4), 1fr);
gap: 0.5em;
width: 100%;
height: 100%;
padding: 0.5em;
justify-items: center;
align-items: center;
}
@@ -0,0 +1,30 @@
.container {
height: 100%;
padding: 0.25em;
display: grid;
grid-template-columns: 180px 1fr;
grid-template-rows: 64px 0.5fr;
gap: 1em 1em;
grid-auto-flow: row;
grid-template-areas:
"header header"
"scan scanner"
}
.header {
align-self: stretch;
grid-area: header;
}
.scanner {
justify-self: center;
align-self: center;
grid-area: scanner;
}
.scan {
align-self: start;
grid-area: scan;
}
@@ -0,0 +1,21 @@
:root{
--track-height:10px;
--track-color:#ddd;
--range-color:#4aa3ff;
--thumb-size:18px;
--thumb-color:#fff;
--thumb-border:#4aa3ff;
}
.slider-container{ width:100%; max-width:640px; margin:32px auto; }
.labels{ display:flex; justify-content:space-between; margin-bottom:12px; font-size:14px; }
.range-wrap{ position:relative; height:var(--thumb-size); user-select:none; touch-action:none; }
.track{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%); height:var(--track-height); background:var(--track-color); border-radius:999px; }
.range-highlight{ position:absolute; top:50%; transform:translateY(-50%); height:var(--track-height); background:var(--range-color); border-radius:999px; }
input[type=range]{ -webkit-appearance:none; appearance:none; position:absolute; left:0; right:0; top:0; width:100%; height:100%; background:transparent; pointer-events:none; }
input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; pointer-events:auto; width:var(--thumb-size); height:var(--thumb-size); border-radius:50%; background:var(--thumb-color); border:2px solid var(--thumb-border); box-shadow:0 1px 3px rgba(0,0,0,0.15); cursor:pointer; margin-top: calc((var(--track-height) - var(--thumb-size)) / 2); }
input[type=range]::-moz-range-thumb{ pointer-events:auto; width:var(--thumb-size); height:var(--thumb-size); border-radius:50%; background:var(--thumb-color); border:2px solid var(--thumb-border); box-shadow:0 1px 3px rgba(0,0,0,0.15); cursor:pointer; }
input[type=range]::-moz-range-track{ background:transparent; border:0; }
@media (max-width:520px){ .values{ flex-direction:column; align-items:flex-start; } .readable{ min-width:unset; width:100%; } }
@@ -0,0 +1,39 @@
.multiwell_cards {
display: grid;
/*grid-auto-columns: 1fr;
grid-auto-rows: 1fr;*/
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: 0.1em;
grid-template-areas:
". ."
". ."
". .";
}
button.multiwell {
padding: 0.15em ;
}
#replay-grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns, 1), 1fr);
gap: 0.5em;
width: 100%;
height: 100%;
padding: 0.5em;
justify-items: center;
align-items: center;
}
.replay-slider, .replay-sub-slide, .replay_ts_cursor{
width: 100%;
flex: 0 0 auto;
}
.replay-timeline {
width: 100%;
background: transparent;
display: block;
}
@@ -0,0 +1,31 @@
.container {
padding: 0.25em;
display: grid;
grid-template-columns: 0.25fr 1.5fr;
grid-template-rows: 0.5fr 3fr 0.5fr;
grid-auto-columns: 1fr;
gap: 1em 1em;
grid-auto-flow: row;
grid-template-areas:
"header header"
"move scanner"
"command command";
}
.header {
align-self: stretch;
grid-area: header;
}
.command { grid-area: command; }
.scanner {
justify-self: center;
align-self: center;
grid-area: scanner;
}
.move {
align-self: stretch;
grid-area: move;
}
@@ -0,0 +1,138 @@
class ScannerManager {
constructor(container) {
this.container = container;
this.socket = null;
this.axes = 0;
this.cropping = 0;
this.debug_count = 0
}
toggle_median() { this.axes = !this.axes; return this.axes; }
toggle_crop() { this.croping = !this.croping; return this.croping; }
init_controls() {
this.ts = sId("_ts");
const goto_0 = sId("_goto-0");
const goto_xy = sId("_goto-xy");
const xy_base = sId("_xy-base");
const xy_step = sId("_xy-step");
const up = sId("_up");
const down = sId("_down");
const left = sId("_left");
const right = sId("_right");
this.feed = sId("_feed");
this.step = sId("_step");
this.well = sId("_well");
this.x = sId("_x");
this.y = sId("_y");
this.dx = sId("_dx");
this.dy = sId("_dy");
this.xbase = sId("_xbase");
this.ybase = sId("_ybase");
this.debug = sId("_debug");
const test = sId("_test");
const halt = sId("_halt");
const median = sId("_median");
const crop = sId("_crop");
const crop_radius = sId("_crop_radius");
up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); });
down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); });
left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); });
right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); });
goto_0.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_0" }); });
goto_xy.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_xy" }); });
xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); });
xy_step.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_step" }); });
median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); });
crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); });
crop_radius.addEventListener('change',(e) => { this._send({ type: 'calibrate', topic: "crop_radius", value: crop_radius.value }); });
this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "position", value: e.target.value }); });
this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); });
this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); });
this.dx.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dx", value: e.target.value }); });
this.dy.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dy", value: e.target.value }); });
test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
}
registerSocket(socket) {
this.socket = socket;
this.init_controls();
}
update(payload) {
try {
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
if (payload.xbase) { this.xbase.textContent = payload.xbase; this.ybase.textContent = payload.ybase; }
if (payload.dxy) { this.dy.value=payload.dy; this.dx.value=payload.dx; }
if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
} catch(e) { console.log(e); }
}
init() {
this.axes = 0;
this.cropping = 0;
this._send({
type: 'calibrate',
topic: "init",
feed: this.feed.value,
step: this.step.value,
position: this.well.value
});
}
start() { this._send({ type: 'scanner', topic: "start"}); }
halt() { this._send({ type: 'scanner', topic: "halt" }); }
_send(message) { this.socket.send(message); }
}
class MetadataSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.manager = null;
this.reconnectDelay = 1000;
this.shouldReconnect = true;
this.reconnect = false;
}
setManager(manager) { this.manager = manager; }
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.manager.update(data);
};
this.ws.onopen = (event) => {
if (this.manager && !this.reconnect)
this.manager['init']();
this.reconnect = false;
};
this.ws.onclose = () => {
console.warn(`WebSocket closed...`);
if (this.shouldReconnect) {
this.reconnect = true;
setTimeout(() => {
console.log("Reconnect WebSocket...");
this.connect();
}, this.reconnectDelay);
}
};
}
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
}
@@ -0,0 +1,92 @@
class ScannerManager {
constructor(container) {
this.container = container;
this.socket = null;
this.axes = 0;
this.cropping = 1;
this.debug_count = 0
}
toggle_median() { this.axes = !this.axes; return this.axes; }
toggle_crop() { this.croping = !this.croping; return this.croping; }
init_controls() {
this.session= sId("_session");
this.ts = sId("_ts");
this.x = sId("_x");
this.y = sId("_y");
this.debug = sId("_debug");
const scan = sId("_scan");
const halt = sId("_halt");
const median = sId("_median");
const crop = sId("_crop");
median.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); });
crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); });
scan.addEventListener('click', (e) => { this.scan(); });
halt.addEventListener('click', (e) => { this.halt(); });
}
registerSocket(socket) {
this.socket = socket;
this.init_controls();
}
update(payload) {
try {
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
} catch(e) { console.log(e); }
}
init() { this.axes = 0; this.cropping = 1; this._send({ type: 'scanner', topic: "init", }); }
scan() { this._send({ type: 'scanner', topic: "scan", session: this.session.value ? this.session.value: "0" }); }
halt() { this._send({ type: 'calibrate', topic: "halt" }); }
_send(message) { this.socket.send(message); }
}
class MetadataSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.manager = null;
this.reconnectDelay = 1000;
this.shouldReconnect = true;
this.reconnect = false;
}
setManager(manager) { this.manager = manager; }
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.manager.update(data);
};
this.ws.onopen = (event) => {
if (this.manager && !this.reconnect)
this.manager['init']();
this.reconnect = false;
};
this.ws.onclose = () => {
console.warn(`WebSocket closed...`);
if (this.shouldReconnect) {
this.reconnect = true;
setTimeout(() => {
console.log("Reconnect WebSocket...");
this.connect();
}, this.reconnectDelay);
}
};
}
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
}
@@ -0,0 +1,220 @@
class DualRangeSlider {
constructor(root, options = {}) {
// root: element containing the slider UI
this.root = (typeof root === 'string') ? document.querySelector(root) : root;
if (!this.root) throw new Error('Root element not found');
// elements
this.slider = this.root.querySelector('.slider');
this.minRange = this.root.querySelector('.min-range');
this.maxRange = this.root.querySelector('.max-range');
this.minNumber = this.root.querySelector('.min-number');
this.maxNumber = this.root.querySelector('.max-number');
this.displayRange = this.root.querySelector('.display-range');
this.highlight = this.root.querySelector('.range-highlight');
// config
this.min = Number(this.slider.dataset.min ?? this.minRange.min ?? 0);
this.max = Number(this.slider.dataset.max ?? this.minRange.max ?? 100);
this.gap = Number(options.gap ?? 0);
this._bindEvents();
this.updateFromInputs();
}
// calc & repaint
updateHighlight() {
const a = Number(this.minRange.value);
const b = Number(this.maxRange.value);
const pctA = (a - this.min) / (this.max - this.min) * 100;
const pctB = (b - this.min) / (this.max - this.min) * 100;
this.highlight.style.left = pctA + '%';
this.highlight.style.width = (pctB - pctA) + '%';
}
// keep values coherent and sync UI
updateFromInputs(triggerSource) {
let a = Number(this.minRange.value);
let b = Number(this.maxRange.value);
if (a > b - this.gap) {
if (triggerSource === 'min') a = b - this.gap;
else if (triggerSource === 'max') b = a + this.gap;
else a = Math.min(a, b - this.gap);
}
a = Math.max(this.min, Math.min(a, this.max));
b = Math.max(this.min, Math.min(b, this.max));
this.minRange.value = a;
this.maxRange.value = b;
this.minNumber.value = a;
this.maxNumber.value = b;
//if (this.displayRange) this.displayRange.textContent = a + ' — ' + b;
if (this.displayRange)
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${a}</div><div class="w3-half w3-right-align">${b}</div></div>`;
this.updateHighlight();
}
updateFromNumbers() {
let a = Number(this.minNumber.value) || this.min;
let b = Number(this.maxNumber.value) || this.max;
if (a > b - this.gap) a = b - this.gap;
a = Math.max(this.min, Math.min(a, this.max));
b = Math.max(this.min, Math.min(b, this.max));
this.minRange.value = a;
this.maxRange.value = b;
this.minNumber.value = a;
this.maxNumber.value = b;
//if (this.displayRange) this.displayRange.textContent = a + ' — ' + b;
if (this.displayRange)
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${a}</div><div class="w3-half w3-right-align">${b}</div></div>`;
this.updateHighlight();
}
_bindEvents() {
this.minRange.addEventListener('input', () => this.updateFromInputs('min'));
this.maxRange.addEventListener('input', () => this.updateFromInputs('max'));
this.minNumber.addEventListener('change', () => this.updateFromNumbers());
this.maxNumber.addEventListener('change', () => this.updateFromNumbers());
// click on track reposition nearest thumb
this.slider.addEventListener('click', (e) => {
if (e.target.tagName.toLowerCase() === 'input') return;
const rect = this.slider.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const pct = clickX / rect.width;
const value = Math.round(pct * (this.max - this.min) + this.min);
const distMin = Math.abs(value - Number(this.minRange.value));
const distMax = Math.abs(value - Number(this.maxRange.value));
if (distMin <= distMax) {
this.minRange.value = Math.min(value, Number(this.maxRange.value));
} else {
this.maxRange.value = Math.max(value, Number(this.minRange.value));
}
this.updateFromInputs();
});
}
// public API: get current values
getValues() {
return { min: Number(this.minRange.value), max: Number(this.maxRange.value) };
}
// public API: set values programmatically
setValues(a, b) {
this.minRange.value = a;
this.maxRange.value = b;
this.updateFromInputs();
}
}
/*
// Exemple d'usage
document.addEventListener('DOMContentLoaded', () => {
// initialisation simple
const slider1 = new DualRangeSlider('#myRangeSlider', { gap: 0 });
// accéder aux valeurs
// console.log(slider1.getValues());
// modifier les valeurs depuis le code
// slider1.setValues(10, 50);
});
*/
/**
* Crée le DOM du slider et instancie DualRangeSlider à partir d'options.
* Retourne { instance, root } ou null si erreur.
*
* options = {
* container: selector|Element, // obligatoire
* min: number, max: number, // obligatoire (timestamps)
* valueMin: number, valueMax: number, // optionnel
* ms: boolean, // unité interne en ms
* gap: number, // minimal gap (same unit as min/max)
* classes: { root, slider, ... } // opcional CSS classes overrides
* labels: { left, right } // opcional text
* }
*/
function createAndInitDualRange(options = {}) {
if (!options.container) throw new Error('options.container required');
const parent = (typeof options.container === 'string') ? document.querySelector(options.container) : options.container;
if (!parent) throw new Error('Container element not found');
if (typeof options.min === 'undefined' || typeof options.max === 'undefined') {
throw new Error('options.min and options.max required');
}
const cls = Object.assign({
root: 'slider-container',
display: 'display-range',
rangeWrap: 'range-wrap slider',
sliderSel: 'slider',
track: 'track',
highlight: 'range-highlight',
minRange: 'min-range',
maxRange: 'max-range',
values: 'values',
minNumber: 'min-number',
maxNumber: 'max-number',
}, options.classes || {});
// build root
const root = document.createElement('div');
root.className = cls.root;
// labels
const labels = document.createElement('div');
labels.className = cls.display;
root.appendChild(labels);
// range wrap / slider
const rangeWrap = document.createElement('div');
rangeWrap.className = cls.rangeWrap;
// set data-min/data-max as provided
rangeWrap.dataset.min = String(options.min);
rangeWrap.dataset.max = String(options.max);
const track = document.createElement('div'); track.className = cls.track;
const highlight = document.createElement('div'); highlight.className = cls.highlight;
rangeWrap.appendChild(track);
rangeWrap.appendChild(highlight);
// inputs range
const minR = document.createElement('input'); minR.type = 'range'; minR.className = cls.minRange;
const maxR = document.createElement('input'); maxR.type = 'range'; maxR.className = cls.maxRange;
minR.min = String(options.min); minR.max = String(options.max);
maxR.min = String(options.min); maxR.max = String(options.max);
minR.step = options.step ?? '1'; maxR.step = options.step ?? '1';
minR.value = String(options.valueMin ?? options.min);
maxR.value = String(options.valueMax ?? options.max);
rangeWrap.appendChild(minR); rangeWrap.appendChild(maxR);
root.appendChild(rangeWrap);
// values area
const values = document.createElement('div'); values.className = "w3-row";
const minLabel = document.createElement('div'); minLabel.className = "w3-half";
const minStrong = document.createElement('span'); minStrong.textContent = 'Min)';
const minNum = document.createElement('input'); minNum.type = 'number'; minNum.className = cls.minNumber;
minNum.min = String(options.min); minNum.max = String(options.max); minNum.value = String(options.valueMin ?? options.min); minNum.step = options.step ?? '1';
minLabel.appendChild(minStrong); minLabel.appendChild(minNum);
const maxLabel = document.createElement('div'); maxLabel.className = "w3-half w3-right-align";
const maxStrong = document.createElement('span'); maxStrong.textContent = 'Max';
const maxNum = document.createElement('input'); maxNum.type = 'number'; maxNum.className = cls.maxNumber;
maxNum.min = String(options.min); maxNum.max = String(options.max); maxNum.value = String(options.valueMax ?? options.max);maxNum.step = options.step ?? '1';
maxLabel.appendChild(maxStrong); maxLabel.appendChild(maxNum);
values.appendChild(minLabel); values.appendChild(maxLabel);
root.appendChild(values);
// append to parent
parent.appendChild(root);
const instOptions = { gap: options.gap ?? 0, ms: Boolean(options.ms), callback: options.callback ?? null };
const instance = new DualRangeSlider(root, instOptions);
return instance;
}
@@ -0,0 +1,265 @@
class DualRangeSlider {
constructor(root, options = {}) {
this.root = (typeof root === 'string') ? document.querySelector(root) : root;
if (!this.root) throw new Error('Root element not found');
this.slider = this.root.querySelector('.slider');
this.minRange = this.root.querySelector('.min-range');
this.maxRange = this.root.querySelector('.max-range');
this.minNumber = this.root.querySelector('.min-number');
this.maxNumber = this.root.querySelector('.max-number');
this.displayRange = this.root.querySelector('.display-range');
this.highlight = this.root.querySelector('.range-highlight');
this.useMilliseconds = Boolean(options.ms);
this.callback = options.callback || null;
// lire min/max depuis data- ou attributs, puis normaliser en unité interne
const rawMin = Number(this.slider.dataset.min ?? this.minRange.min ?? 0);
const rawMax = Number(this.slider.dataset.max ?? this.minRange.max ?? 100);
// unité interne = ms si useMilliseconds true, sinon seconds
this.min = this.useMilliseconds ? rawMin : rawMin;
this.max = this.useMilliseconds ? rawMax : rawMax;
// Si ms=true mais HTML inputs fournis en secondes, détecter et convertir automatiquement
// Détection simple : si ms=true et raw values semblent petits (<1e12), on multiplie par 1000
if (this.useMilliseconds) {
const needsConversion = (n) => n && n < 1e12; // timestamp en s
if (needsConversion(this.min)) this.min *= 1000;
if (needsConversion(this.max)) this.max *= 1000;
} else {
// si ms=false mais raw semblent en ms (>1e12), convertir en s
const needsConversion = (n) => n && n > 1e12;
if (needsConversion(this.min)) this.min = Math.floor(this.min / 1000);
if (needsConversion(this.max)) this.max = Math.floor(this.max / 1000);
}
// normaliser les inputs HTML pour qu'ils correspondent à l'unité interne
const setInputRangeAttrs = (el, val) => {
if (!el) return;
if (this.useMilliseconds) el.min = String(this.min);
else el.min = String(this.min);
if (this.useMilliseconds) el.max = String(this.max);
else el.max = String(this.max);
// si value absent, initialiser
if (!el.value) el.value = el.min;
};
setInputRangeAttrs(this.minRange);
setInputRangeAttrs(this.maxRange);
if (this.minNumber) { this.minNumber.min = String(this.min); this.minNumber.max = String(this.max); }
if (this.maxNumber) { this.maxNumber.min = String(this.min); this.maxNumber.max = String(this.max); }
this.gap = Number(options.gap ?? 0) * (this.useMilliseconds ? 1000 : 1);
// if inputs have values, ensure they are in internal unit; convert if necessary
const normalizeInputValue = (el) => {
if (!el) return;
let v = Number(el.value || el.getAttribute('value') || this.min);
if (this.useMilliseconds && v < 1e12) v = v * 1000;
if (!this.useMilliseconds && v > 1e12) v = Math.floor(v / 1000);
el.value = String(Math.max(this.min, Math.min(v, this.max)));
};
normalizeInputValue(this.minRange);
normalizeInputValue(this.maxRange);
normalizeInputValue(this.minNumber);
normalizeInputValue(this.maxNumber);
this._bindEvents();
this.updateFromInputs();
}
_toDate(ts) {
// ts is in internal unit: ms if useMilliseconds else seconds
return this.useMilliseconds ? new Date(Number(ts)) : new Date(Number(ts) * 1000);
}
_formatDate(ts) {
const d = this._toDate(ts);
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
updateHighlight() {
const a = Number(this.minRange.value);
const b = Number(this.maxRange.value);
if (this.callback) this.callback(a, b);
const pctA = (a - this.min) / (this.max - this.min) * 100;
const pctB = (b - this.min) / (this.max - this.min) * 100;
this.highlight.style.left = pctA + '%';
this.highlight.style.width = (pctB - pctA) + '%';
}
updateFromInputs(triggerSource) {
let a = Number(this.minRange.value);
let b = Number(this.maxRange.value);
if (a > b - this.gap) {
if (triggerSource === 'min') a = b - this.gap;
else if (triggerSource === 'max') b = a + this.gap;
else a = Math.min(a, b - this.gap);
}
a = Math.max(this.min, Math.min(a, this.max));
b = Math.max(this.min, Math.min(b, this.max));
this.minRange.value = String(a);
this.maxRange.value = String(b);
if (this.minNumber) this.minNumber.value = String(a);
if (this.maxNumber) this.maxNumber.value = String(b);
//if (this.displayRange) this.displayRange.textContent = `${this._formatDate(a)} — ${this._formatDate(b)}`;
if (this.displayRange)
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${this._formatDate(a)}</div><div class="w3-half w3-right-align">${this._formatDate(b)}</div></div>`;
this.updateHighlight();
}
updateFromNumbers() {
let a = Number(this.minNumber.value) || this.min;
let b = Number(this.maxNumber.value) || this.max;
if (a > b - this.gap) a = b - this.gap;
a = Math.max(this.min, Math.min(a, this.max));
b = Math.max(this.min, Math.min(b, this.max));
this.minRange.value = String(a);
this.maxRange.value = String(b);
this.minNumber.value = String(a);
this.maxNumber.value = String(b);
//if (this.displayRange) this.displayRange.textContent = `${this._formatDate(a)} — ${this._formatDate(b)}`;
if (this.displayRange)
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${this._formatDate(a)}</div><div class="w3-half w3-right-align">${this._formatDate(b)}</div></div>`;
this.updateHighlight();
}
_bindEvents() {
this.minRange.addEventListener('input', () => this.updateFromInputs('min'));
this.maxRange.addEventListener('input', () => this.updateFromInputs('max'));
if (this.minNumber) this.minNumber.addEventListener('change', () => this.updateFromNumbers());
if (this.maxNumber) this.maxNumber.addEventListener('change', () => this.updateFromNumbers());
this.slider.addEventListener('click', (e) => {
if (e.target.tagName.toLowerCase() === 'input') return;
const rect = this.slider.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const pct = clickX / rect.width;
const value = Math.round(pct * (this.max - this.min) + this.min);
const distMin = Math.abs(value - Number(this.minRange.value));
const distMax = Math.abs(value - Number(this.maxRange.value));
if (distMin <= distMax) {
this.minRange.value = String(Math.min(value, Number(this.maxRange.value)));
} else {
this.maxRange.value = String(Math.max(value, Number(this.minRange.value)));
}
this.updateFromInputs();
});
}
getValues() {
return { min: Number(this.minRange.value), max: Number(this.maxRange.value) };
}
setValues(a, b) {
// a,b given in same unit as useMilliseconds setting
this.minRange.value = String(a);
this.maxRange.value = String(b);
this.updateFromInputs();
}
}
/**
* Crée le DOM du slider et instancie DualRangeSlider à partir d'options.
* Retourne { instance, root } ou null si erreur.
*
* options = {
* container: selector|Element, // obligatoire
* min: number, max: number, // obligatoire (timestamps)
* valueMin: number, valueMax: number, // optionnel
* ms: boolean, // unité interne en ms
* gap: number, // minimal gap (same unit as min/max)
* classes: { root, slider, ... } // opcional CSS classes overrides
* labels: { left, right } // opcional text
* }
*/
function createAndInitDualRange(options = {}) {
if (!options.container) throw new Error('options.container required');
const parent = (typeof options.container === 'string') ? document.querySelector(options.container) : options.container;
if (!parent) throw new Error('Container element not found');
if (typeof options.min === 'undefined' || typeof options.max === 'undefined') {
throw new Error('options.min and options.max required');
}
const cls = Object.assign({
root: 'slider-container',
display: 'display-range',
rangeWrap: 'range-wrap slider',
sliderSel: 'slider',
track: 'track',
highlight: 'range-highlight',
minRange: 'min-range',
maxRange: 'max-range',
values: 'values',
minNumber: 'min-number',
maxNumber: 'max-number',
}, options.classes || {});
// build root
const root = document.createElement('div');
root.className = cls.root;
// labels
const labels = document.createElement('div');
labels.className = cls.display;
root.appendChild(labels);
// range wrap / slider
const rangeWrap = document.createElement('div');
rangeWrap.className = cls.rangeWrap;
// set data-min/data-max as provided
rangeWrap.dataset.min = String(options.min);
rangeWrap.dataset.max = String(options.max);
const track = document.createElement('div'); track.className = cls.track;
const highlight = document.createElement('div'); highlight.className = cls.highlight;
rangeWrap.appendChild(track);
rangeWrap.appendChild(highlight);
// inputs range
const minR = document.createElement('input'); minR.type = 'range'; minR.className = cls.minRange;
const maxR = document.createElement('input'); maxR.type = 'range'; maxR.className = cls.maxRange;
minR.min = String(options.min); minR.max = String(options.max);
maxR.min = String(options.min); maxR.max = String(options.max);
minR.step = options.step ?? '1'; maxR.step = options.step ?? '1';
minR.value = String(options.valueMin ?? options.min);
maxR.value = String(options.valueMax ?? options.max);
rangeWrap.appendChild(minR); rangeWrap.appendChild(maxR);
root.appendChild(rangeWrap);
// values area
const values = document.createElement('div'); values.className = "w3-row";
const minLabel = document.createElement('div'); minLabel.className = "w3-half";
const minStrong = document.createElement('span'); minStrong.textContent = 'Min (ms)';
const minNum = document.createElement('input'); minNum.type = 'number'; minNum.className = cls.minNumber;
minNum.min = String(options.min); minNum.max = String(options.max); minNum.value = String(options.valueMin ?? options.min); minNum.step = options.step ?? '1';
minLabel.appendChild(minStrong); minLabel.appendChild(minNum);
const maxLabel = document.createElement('div'); maxLabel.className = "w3-half w3-right-align";
const maxStrong = document.createElement('span'); maxStrong.textContent = 'Max (ms)';
const maxNum = document.createElement('input'); maxNum.type = 'number'; maxNum.className = cls.maxNumber;
maxNum.min = String(options.min); maxNum.max = String(options.max); maxNum.value = String(options.valueMax ?? options.max);maxNum.step = options.step ?? '1';
maxLabel.appendChild(maxStrong); maxLabel.appendChild(maxNum);
values.appendChild(minLabel); values.appendChild(maxLabel);
root.appendChild(values);
// append to parent
parent.appendChild(root);
const instOptions = { gap: options.gap ?? 0, ms: Boolean(options.ms), callback: options.callback ?? null };
const instance = new DualRangeSlider(root, instOptions);
return instance;
}
@@ -0,0 +1,260 @@
class ReplayProgressBar {
constructor(parent, input) {
this.input = input;
this.parent = parent;
this.isUserSeeking = false;
input.addEventListener("input", () => {
this.isUserSeeking = true;
const percent = input.value / 1000;
this.parent.seek(percent);
});
input.addEventListener("change", () => {
this.isUserSeeking = false;
});
}
update(progress) {
if (this.isUserSeeking) return;
this.input.value = progress * 1000;
}
}
class ReplaySpeedControl {
constructor(parent, initialSpeed = 1) {
this.parent = parent;
this.SPEED_STEPS = [0.25, 0.5, 0.75, 1, 2, 4, 8, 16, 32];
const initialIndex = this.SPEED_STEPS.indexOf(initialSpeed) !== -1
? this.SPEED_STEPS.indexOf(initialSpeed)
: this.SPEED_STEPS.indexOf(1);
this.parent.speed_control.addEventListener("input", () => {
this.parent.speed_label.textContent = this.SPEED_STEPS[this.parent.speed_control.value];
});
this.parent.speed_control.addEventListener("change", () => {
const speed = this.getSpeed();
this.parent.setSpeed(speed);
});
}
getSpeed() {
return this.SPEED_STEPS[this.parent.speed_control.value];
}
updateSpeed(speed) {
this.parent.speed_control.value = this.SPEED_STEPS.indexOf(speed);
this.parent.speed_label.textContent = this.SPEED_STEPS[this.parent.speed_control.value];
}
}
class ReplayManager {
constructor(options = {}) {
this.img = options.img;
this.btplay = options.play;
this.btpause= options.pause;
this.btstop = options.stop;
this.ts = options.ts;
this.btsnapshot = options.snapshot;
this.btvideosnap = options.videosnap;
this.speed_label = options.speed_label;
this.speed_control = options.speed_control;
this.ts_iso = options.ts_iso;
this.percent = options.percent;
this.dt_left = options.dt_left;
this.dt_right = options.dt_right;
this.timeline = options.timeline;
this.uuid = options.uuid;
this.fps = options.fps;
this.video_endpoint = options.video_endpoint;
this.dt_start = options.dt_start;
this.dt_stop = options.dt_stop;
this.video_type = options.video_type || 'mp4';
this.state = null;
this.socket = null;
this.cursor = null;
}
registerSocket(socket) {
this.socket = socket;
}
async start() {
if (!this.uuid) return;
this.btplay.addEventListener('click', (e) => { this.play(); });
this.btpause.addEventListener('click', (e) => { this.pause(); });
this.btstop.addEventListener('click', (e) => { this.stop(); });
this.btsnapshot.addEventListener('click', (e) => { this.snapshot(); });
this.btvideosnap.addEventListener('click', (e) => { this.videosnap(); });
this.speedControl = new ReplaySpeedControl(this);
this.progressBar = new ReplayProgressBar(this, this.timeline);
this._update_container(this.dt_start);
this.updateState("stopped");
}
updateProgessBar(progress) {
if (this.progressBar) {
this.progressBar.update(progress);
}
}
updateState(state) {
const rules = {
stopped: { play: true, pause: false, stop: false, range: true, snapshot: true, videosnap: true },
playing: { play: false, pause: true, stop: true, range: false , snapshot: false, videosnap: false },
paused: { play: true, pause: false, stop: true, range: true , snapshot: true, videosnap: true }
};
const rule = rules[state];
this.btplay.disabled = !rule.play;
this.btpause.disabled = !rule.pause;
this.btstop.disabled = !rule.stop;
this.btsnapshot.disabled = !rule.snapshot;
this.btvideosnap.disabled = !rule.videosnap;
this.timeline.disabled = rule.range;
}
_setState(state) {
this.state = state;
this.updateState(state);
}
_update_container(cursor) {
this.cursor = cursor;
this.filter = {
uuid: this.uuid,
dt_start: cursor,
dt_stop: this.dt_stop,
fps: this.fps,
speed: this.speedControl.getSpeed()
}
this.percent.textContent = '';
this.updateProgessBar(0.0);
const dt = cursor / 1_000;
this.ts_iso.textContent = toLocalISOString(new Date(dt));
this.dt_left.textContent = timestampToLocalISOString(dt/1_000);
this.dt_right.textContent = timestampToLocalISOString(this.dt_stop/1_000_000);
this._setState("stopped");
}
_update_content_slider(ts, progress) {
this.cursor = ts;
this.ts_iso.textContent = toLocalISOString(new Date(ts/1000));
const percent = Math.ceil(progress * 1000000) / 10000;
this.percent.textContent = percent.toFixed(3) +' %';
this.updateProgessBar(progress);
}
update(payload) {
try {
if (payload.jpeg) { this.img.src = `data:image/jpeg;base64,${payload.jpeg}`; }
if (payload.ts) { this._update_content_slider(payload.ts, payload.progress); }
if (payload.motif === "video-reset") { this._update_container(payload.dt_start); }
} catch(e) { console.log(e); }
}
init() { this._send({ type: 'replay', action: "init", }); }
play() { if (this.state === "playing") return; this._setState("playing"); this._send({ type: 'replay', action: "play", }); }
pause() { if (this.state !== "playing") return; this._setState("paused"); this._send({ type: 'replay', action: "pause", }); }
stop() { this._setState("stopped"); this._send({ type: 'replay', action: "stop", }); }
setSpeed(speed) { this._send({ type: 'replay', action: "speed", value: speed }); }
seek(percent) { this._send({ type: 'replay', action: "seek", value: percent }); }
videosnap() {
let filename = `${this.uuid}-${this.ts_iso.textContent}.${this.video_type}`;
filename = filename.replace(/ /g, '_');
const ok = confirm(`Télécharger le fichier ?\n\n${filename}`);
if (!ok) return false;
fetch(this.video_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'download',
uuid: this.uuid,
dt_start: this.cursor,
dt_stop: this.dt_stop,
fps: this.fps
})
}).then(res => {
if (!res.ok) throw new Error(res.status);
return res.blob();
}).then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}).catch(error => {
console.error('Erreur:', error);
});
}
snapshot() {
let filename = `${this.uuid}-${this.ts_iso.textContent}.jpg`;
filename = filename.replace(/ /g, '_');
const ok = confirm(`Télécharger le fichier ?\n\n${filename}`);
if (!ok) return false;
const a = document.createElement('a');
a.href = this.img.src;
a.download = filename || 'image';
document.body.appendChild(a);
a.click();
a.remove();
}
_send(message) {
if (!this.uuid) return;
this.socket.send({ ...message, ...this.filter });
}
}
class MetadataSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.manager = null;
this.reconnectDelay = 1000;
this.shouldReconnect = true;
this.reconnect = false;
}
setManager(manager) { this.manager = manager; }
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.manager.update(data);
};
this.ws.onopen = (event) => {
if (this.manager && !this.reconnect)
this.manager['init']();
this.reconnect = false;
};
this.ws.onclose = () => {
console.warn(`WebSocket closed...`);
if (this.shouldReconnect) {
this.reconnect = true;
setTimeout(() => {
console.log("Reconnect WebSocket...");
this.connect();
}, this.reconnectDelay);
}
};
}
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
}
@@ -0,0 +1,132 @@
class ScannerManager {
constructor(container, multiwells=null) {
this.container = container;
this.socket = null;
this.multiweels = multiwells;
this.axes = 0;
this.cropping = 0;
}
toggle_median() { this.axes = !this.axes; return this.axes; }
toggle_crop() { this.croping = !this.croping; return this.croping; }
init_controls() {
const goto_0 = sId("_goto-0");
const goto_xy = sId("_goto-xy");
const xy_base = sId("_xy-base");
const xy_step = sId("_xy-step");
const up = sId("_up");
const down = sId("_down");
const left = sId("_left");
const right = sId("_right");
this.feed = sId("_feed");
this.step = sId("_step");
this.well = sId("_well");
this.x = sId("_x");
this.y = sId("_y");
this.dx = sId("_dx");
this.dy = sId("_dy");
this.xbase = sId("_xbase");
this.ybase = sId("_ybase");
const test = sId("_test");
const halt = sId("_halt");
const median = sId("_median");
const crop = sId("_crop");
up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); });
down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); });
left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); });
right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); });
goto_0.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_0" }); });
goto_xy.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_xy" }); });
xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); });
xy_step.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_step" }); });
median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); });
crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); });
this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "well", value: e.target.value }); });
this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); });
this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); });
this.dx.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dx", value: e.target.value }); });
this.dy.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dy", value: e.target.value }); });
test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
}
registerSocket(socket) {
this.socket = socket;
this.init_controls();
}
update(payload) {
try {
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
if (payload.xbase) { this.xbase.textContent = payload.xbase; this.ybase.textContent = payload.ybase; }
if (payload.dxy) { this.dy.value=payload.dy; this.dx.value=payload.dx; }
if (payload.xy) { this.x.textContent=payload.x; this.y.textContent=payload.y; }
//if (payload.ts) { console.log(payload.ts); }
} catch(e) { console.log(e); }
}
init() {
this._send({
type: 'scanner',
topic: "init",
feed: this.feed.value,
step: this.step.value,
well: this.well.value
});
}
start() { this._send({ type: 'scanner', topic: "start"}); }
halt() { this._send({ type: 'scanner', topic: "halt" }); }
_send(message) { this.socket.send(message); }
}
class MetadataSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.manager = null;
this.reconnectDelay = 1000;
this.shouldReconnect = true;
this.reconnect = false;
}
setManager(manager) { this.manager = manager; }
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.manager.update(data);
};
this.ws.onopen = (event) => {
if (this.manager && !this.reconnect)
this.manager['init']();
this.reconnect = false;
};
this.ws.onclose = () => {
console.warn(`WebSocket closed...`);
if (this.shouldReconnect) {
this.reconnect = true;
setTimeout(() => {
console.log("Reconnect WebSocket...");
this.connect();
}, this.reconnectDelay);
}
};
}
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
}
@@ -0,0 +1,31 @@
const cpu_used = sId('cpu-used');
const shm_used = sId('shm-used');
const mem_used = sId('mem-used');
const disk_used = sId('disk-used');
const ramdisk_used = sId('ramdisk-used');
let autoTimer = null;
async function fetchStats() {
try {
const r = await fetch(stats_endpoint, { credentials: 'same-origin' });
if (!r.ok) throw new Error('HTTP ' + r.status);
const j = await r.json();
//console.log(j);
const cpu_percent = j.cpu_info.cpu_percent+'%'; cpu_used.style.setProperty("--cpu-used", cpu_percent); cpu_used.title=`Cpu: ${cpu_percent}`;
const shm_length = j.shm.length; shm_used.style.setProperty("--shm_used", shm_length); shm_used.title= `Shm: ${shm_length}`;
const virtual_memory = j.memory_info.virtual_memory.percent+'%'; mem_used.style.setProperty("--mem-used", virtual_memory); mem_used.title=`Mem: ${virtual_memory}`;
const root_percent = j.disk_info.root.percent+'%'; disk_used.style.setProperty("--disk-used", root_percent); disk_used.title=`Disk: ${root_percent}`;
let ramdisk_percent = "0%";
if (! j.ramdisk_info) ramdisk_percent = j.ramdisk_info.percent+'%';
ramdisk_used.style.setProperty("--ramdisk-used", ramdisk_percent); ramdisk_used.title=`Ramdisk: ${ramdisk_percent}`;
} catch (e) {
console.log('Error: ' + e.message);
}
}
function auto_fetch_start() { fetchStats(); autoTimer = setInterval(fetchStats, 5000);}
function auto_fetch_stop() { clearInterval(autoTimer); autoTimer = null; }
auto_fetch_start();
+243
View File
@@ -0,0 +1,243 @@
# tasks.py
import asyncio
from celery import shared_task, group, chord, chain
from celery.utils.log import get_task_logger
from django.utils import timezone
from .process import ScannerProcess, ReplayProcess
from .export_tasks import shm_download_video, export_images_zip, export_video_mp4
from . import models
logger = get_task_logger(__name__)
class ScannerTaskManager:
def __init__(self):
self.scanner = None
self.replay = None
def start_scanner(self):
if self.scanner is None:
self.scanner = ScannerProcess()
self.scanner.start()
def stop_scanner(self):
if self.scanner:
self.scanner.stop()
def start_replay(self, latency=5.0):
if self.replay is None:
self.replay = ReplayProcess(latency=latency)
self.replay.start()
def stop_replay(self):
if self.replay:
self.replay.stop()
task_manager = ScannerTaskManager()
@shared_task(bind=True)
def scanner_start(self):
task_manager.start_scanner()
return f"Scanner démarré."
#@shared_task(bind=True)
#def scanner_stop(self):
# task_manager.stop_scanner()
# return f"Scanner arrêté."
@shared_task(bind=True)
def replay_start(self):
task_manager.start_replay()
return f"Replay démarré."
#@shared_task(bind=True)
#def replay_stop(self):
# task_manager.stop_replay()
# return f"Replay arrêté."
@shared_task
def download_video(uuid, start_ts, end_ts, frame_rate=10, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
try:
return asyncio.run(shm_download_video(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type))
except Exception as e:
logger.error(f"download_video: {e}")
@shared_task
def export_images(
uuid: str,
start_ts: float | None = None,
end_ts: float | None = None,
jpeg_quality: int = 90,
max_zip_size_mb: int = 0,
max_image_width: int = 0,
max_image_height: int = 0 ):
try:
return asyncio.run(export_images_zip(uuid, start_ts, end_ts, jpeg_quality, max_zip_size_mb, max_image_width, max_image_height))
except Exception as e:
logger.error(f"export_images: {e}")
@shared_task
def export_all_images(session_id=None):
try:
conf = models.Configuration.objects.filter(active=True).first()
if session_id is None:
sessions = [s.id for s in models.Session.objects.filter(active=False)]
else:
sessions = [session_id]
for session_id in sessions:
uuid_list = models.SessionObservation.uuid_from_session(session_id)
job_zip = []
for uuid in uuid_list:
job = export_images.delay( # @UndefinedVariable
uuid,
start_ts=None,
end_ts=None,
jpeg_quality=conf.video_jpeg_quality, # Qualité JPEG
)
job_zip.append(job.id)
return job_zip
except Exception as e:
logger.error(f"export_images: {e}")
@shared_task
def export_videos(uuid: str,
start_ts: float | None = None,
end_ts: float | None = None,
frame_rate: int = 5,
opencv_fourcc_format='mp4v',
opencv_video_type='mp4',
max_video_size_mb: int = 0,
max_width: int = 0,
max_height: int = 0, ):
try:
return asyncio.run(export_video_mp4(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type, max_video_size_mb, max_width, max_height))
except Exception as e:
logger.error(f"export_videos: {e}")
@shared_task
def export_all_videos(session_id=None):
try:
conf = models.Configuration.objects.filter(active=True).first()
if session_id is None:
sessions = [s.id for s in models.Session.objects.filter(active=False)]
else:
sessions = [session_id]
for session_id in sessions:
uuid_list = models.SessionObservation.uuid_from_session(session_id)
job_mp4 = []
for uuid in uuid_list:
job = export_videos.delay( # @UndefinedVariable
uuid,
start_ts=None,
end_ts=None,
frame_rate=conf.video_frame_rate, # Frame rate de la vidéo exportée
opencv_fourcc_format=conf.opencv_fourcc_format, # Format de compression vidéo (ex: 'mp4v' pour MP4),
opencv_video_type=conf.opencv_video_type, # Type de vidéo exportée (ex: 'mp4', 'avi', 'mkv'),
)
job_mp4.append(job.id)
return job_mp4
except Exception as e:
logger.error(f"export_all_videos: {e}")
@shared_task(bind=True)
def run_session_exports(self, session_id: str):
"""
Orchestre l'export images + vidéo en parallèle via chord.
Lance en parallèle l'export images et l'export vidéo de la session.
Le callback on_exports_done est appelé quand les 2 sont terminés.
"""
try:
session = models.Session.objects.get(pk=session_id)
except models.Session.DoesNotExist:
logger.error("run_session_exports: session %s introuvable", session_id)
return {"status": "error", "message": "Session introuvable"}
session.status = models.Session.Status.RUNNING
session.save(update_fields=["export_status"])
chord(
group(
export_all_images.s(session_id),
export_all_videos.s(session_id),
),
on_exports_done.s(session_id=session_id)
).apply_async()
@shared_task
def on_exports_done(results: list, session_id: str):
"""
Callback appelé par chord quand export_all_images ET export_all_videos
sont tous les deux terminés.
results = [résultat_images, résultat_vidéo]
"""
session = models.Session.objects.get(pk=session_id)
errors = [r for r in results if isinstance(r, dict) and r.get("export_status") == "error"]
if errors:
session.export_status = models.Session.Status.ERROR
logger.error(f"on_exports_done [{session_id}]: échec avec {len(errors)} erreurs")
else:
session.export_status = models.Session.Status.DONE
logger.info(f"on_exports_done [{session_id}]: succès export terminé")
session.export_exported_at = timezone.now()
session.save(update_fields=["export_status", "export_exported_at"])
return {"status": session.export_status}
@shared_task
def scanning(session_id: str):
try:
scanner = task_manager.scanner
scanner.cam.set_median(is_median=False)
scanner.cam.set_circular_crop(scanner.crop)
scanner.grbl.go_origin(feed=scanner.manager.feed)
scanner.manager.scan(scanner.grbl, session_id)
except Exception as e:
logger.error(f"scanning session: {session_id} error {e}")
return {"status": "error", "message": str(e)}
@shared_task(bind=True)
def run_scanning(self, session_id: str):
try:
session = models.Session.objects.get(pk=session_id)
if not session.active:
raise Exception("La session n'est plus active")
except models.Session.DoesNotExist:
logger.error("run_session_exports: session %s introuvable", session_id)
return {"status": "error", "message": "Session introuvable"}
session.scanning_status = models.Session.Status.RUNNING
session.save(update_fields=["scanning_status"])
chain(
scanning.s(session_id),
on_scanning_done.s(session_id=session_id),
).apply_async()
@shared_task
def on_scanning_done(result: dict, session_id: str):
"""
Callback appelé automatiquement à la fin de scanning().
Met à jour scanning_status en base — détectable par polling.
"""
try:
session = models.Session.objects.get(pk=session_id)
except models.Session.DoesNotExist:
logger.error("on_scanning_done: session %s introuvable", session_id)
return
session.scanning_status = models.Session.Status.ERROR if result.get("status") == "error" else models.Session.Status.DONE
session.scanning_finished_at = timezone.now()
session.save(update_fields=["scanning_status", "scanning_finished_at"])
@@ -0,0 +1,128 @@
{% extends 'base.html' %}
{% load i18n home_tags %}
{% block header %}
<div class="w3-bar w3-dark-light">
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-xlarge w3-text-yellow" onclick="toggleSidebar()" title="{% trans 'Barre de côté' %}">
<i class="fa-solid fa-right-left"></i>
</a>
<div class="w3-bar-item w3-btn w3-hover-opacity w3-xlarge">
{{ choice_title }}
</div>
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-right w3-xlarge w3-text-green" onclick="goFullscreen()" title="{% trans 'Ecran plein' %}">
<i class="fa-solid fa-maximize"></i>
</a>
</div>
{% endblock %}
{% block sidebar %}
<div class="w3-dark-xlight w3-border-bottom">
<div class="w3-large w3-bold">
<a href="/" class="w3-btn w3-hover-opacity w100 w3-left-align">{{ app_title }}<br>
<span class="w3-tiny">{{ app_sub_title }}</span>
</a>
</div>
<form method="post" action="{% url 'logout' %}" class="w3-padding-small">
{% csrf_token %}
<button class="w3-bar-item w3-btn w3-hover-opacity" type="submit" title="{% trans 'Déconnexion' %}">
{{ request.user.username }}<i class="fa-solid fa-right-from-bracket w3-right"></i>
</button>
</form>
</div>
{% block system %}
<div class="w3-light-blue w3-padding-large">
<div id="shm-used" class="w3-green w3-round" style="height:0.5em; width: var(--shm-used, 0%);" title="{% trans 'Mémoire partagée' %}"></div>
<div id="ramdisk-used" class="w3-purple w3-round" style="height:0.5em;width: var(--ramdisk-used, 0%); margin: 0.35em 0" title="{% trans 'Disque RAM' %}"></div>
<div id="mem-used" class="w3-deep-orange w3-round" style="height:0.5em;width: var(--mem-used, 0%); margin: 0.35em 0" title="{% trans 'Mémoire RAM' %}"></div>
<div id="cpu-used" class="w3-indigo w3-round" style="height:0.5em;width: var(--cpu-used, 0%); margin: 0.35em 0" title="{% trans 'Charge CPU' %}"></div>
<div id="disk-used" class="w3-amber w3-round" style="height:0.5em;width: var(--disk-used, 0%); margin: 0.35em 0" title="{% trans 'Disque /' %}"></div>
</div>
{% endblock %}
{% block columns %}
<div class="w3-dark-light">
<div class=" w3-padding w3-border-bottom">
<div>{% trans "Grille" %}</div>
<div style="width:100%">
{% x_range 1 13 as cols %}
{% for c in cols %}
<button id="swap-{{ c }}" class="w3-btn w3-badge w3-padding-small w3-sand w3-tiny" onclick="setGridColumns({{ c }})">{{ c }}</button>
{% endfor %}
</div>
</div>
</div>
<script>
function updateActiveButton(n) {
document.querySelectorAll('[id^="swap-"]').forEach(btn => {
btn.classList.remove('w3-blue', 'w3-sand'); btn.classList.add('w3-sand');
});
const active = sId('swap-'+ n);
if (active) { active.classList.remove('w3-sand'); active.classList.add('w3-blue');
}
}
</script>
{% endblock %}
{% block sidebar_list %}
<div class="w3-dark-light w3-padding-small">
{% block sidebar_command %}{% endblock %}
{% if request.user.is_superuser %}
<div>
<div class="w3-dark-light w3-margin-top-6">
<a href="#" class="w3-btn w3-padding-small" onclick="toggleDisplay(sId('_admin'))"><span class="w3-text-orange w3-xlarge">&#x26F6;</span>
{% trans "Administration" %}
</a>
</div>
<div id="_admin" style="display:none">
<a href="{% url 'scanner:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Administration base de données" %}
</a>
<a href="{% url 'scanner:reductstore' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<img class="w3-no-padding" src="/static/img/reductstore.png" style="width: 24px"> {% trans "Base de données Reductstore" %}
</a>
<!--a href="{% url 'scanner:adminer' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<img class="w3-no-padding" src="/static/img/adminer.png" style="width: 28px"> {% trans "Base de données mariadb" %}
</a-->
<!--a href="{% url 'scanner:portainer' %}" class="w3-bar-item w3-btn w3-hover-opacity" w3-margin-left>
<img class="w3-no-padding" src="/static/img/portainer.png" style="width: 28px"> {% trans "Portainer" %}
</a-->
<a href="{% url 'scanner:supervisor' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<i class="fa-solid fa-gears w3-text-purple"></i> {% trans "Supervisor" %}
</a>
<a href="{% url 'scanner:logs_scheduler' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<i class="fa-regular fa-file-lines w3-text-orange"></i> {% trans "Logs des planifications" %}
</a>
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des workers" %}
</a>
<a href="{% url 'scanner:calibration' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
<i class="fa-solid fa-wrench w3-text-red"></i> {% trans "Calibration" %}
</a>
</div>
</div>
{% endif %}
{% block sidebar_smenu %}
<a href="{% url 'scanner:main' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-solid fa-film w3-text-green"></i> {% trans "Balayage multi-puits" %}
</a>
<a href="{% url 'scanner:images' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-regular fa-images w3-text-cyan"></i> {% trans "Gestionnaire d'images" %}
</a>
<a href="{% url 'scanner:replay' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-solid fa-podcast w3-text-amber"></i> {% trans "Redifusion vidéos" %}
</a>
{% endblock %}
</div>
{% endblock %}
{% endblock %}
{% block content %}{% endblock %}
{% block js_footer %}
{{ block.super }}
<script>
const stats_endpoint = "{% url 'scanner:api_stats' %}";
</script>
<script src="/static/scanner/js/stats.js"></script>
{% endblock %}
@@ -0,0 +1,122 @@
{% extends 'scanner/base.html' %}
{% load i18n home_tags %}
{% block styles %}
{{ block.super }}
<link href="/static/scanner/css/calibration.css" rel="stylesheet">
{% endblock %}
{% block columns %}{% endblock %}
{% block content %}
<div class="container w3-black">
<div class="header">
<div class="w3-row w3-row-padding">
<div class="w3-col" style="width:30%">
{% trans 'Position multi-puit' %}<br>
<select id="_well" class="w3-select">
{% for w in wells %}
<option value="{{ w.position }}" {% if w.position == 'HG' %}selected{% endif %}>{{ w }}</option>
{% endfor %}
</select>
</div>
<div class="w3-col" style="width:20%">
<div>{% trans 'Vitesse' %}</div>
<select id="_feed" class="w3-select">
<option value="500">500 mm/mn</option>
<option value="750">750 mm/mn</option>
<option value="1000" selected>1000 mm/mn</option>
<option value="1200">1200 mm/mn</option>
<option value="1500">1500 mm/mn</option>
<option value="2000">2000 mm/mn</option>
<option value="3000">3000 mm/mn</option>
</select>
</div>
<div class="w3-col" style="width:20%">
<div>{% trans 'Pas' %}</div>
<select id="_step" class="w3-select">
<option value="0.1">0.1 mm</option>
<option value="0.25">0.25 mm</option>
<option value="0.5">0.5 mm</option>
<option value="1.0" selected>1.0 mm</option>
<option value="1.5">1.5 mm</option>
<option value="2.0">2.0 mm</option>
<option value="2.5">2.5 mm</option>
<option value="5.0">5.0 mm</option>
<option value="10.0">10.0 mm</option>
<option value="20.0">20.0 mm</option>
<option value="30.0">30.0 mm</option>
<option value="40.0">40.0 mm</option>
<option value="50.0">50.0 mm</option>
</select>
</div>
<div class="w3-col" style="width:30%">
<div class="w3-margin-top w3-padding w3-right">
<span id="_ts"></span><br>
</div>
</div>
</div>
</div>
<div class="move w3-padding-small w3-center">
<div class="w3-row">
<div class="w3-half">{% trans 'dx' %}<br><input id="_dx" type="number" min="15.0" max="25.0" step="0.01" value=""/></div>
<div class="w3-half">{% trans 'dy' %}<br><input id="_dy" type="number" min="15.0" max="25.0" step="0.01" value=""/></div>
</div>
<button id="_xy-step" class="w3-button w3-warning w3-round-large w3-block w3-large">
<i class="fa-solid fa-left-right"></i> {% trans 'Définir (dx, dy)' %}
</button>
<hr>
<div><button id="_up" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">&#x2191; {% trans 'En haut' %}</button></div>
<div><button id="_left" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">&#x2190; {% trans 'A gauches' %}</button></div>
<div><button id="_right" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">&#x2192; {% trans 'A droite' %}</button></div>
<div><button id="_down" class="w3-button w3-dark-xlight w3-round-large w3-margin-small w3-block">&#x2193; {% trans 'En Bas' %}</button></div>
<hr>
<button id="_xy-base" class="w3-button w3-warning w3-round-large w3-margin-small w3-block">{% trans 'Définir base ' %}</button>
</div>
<div class="scan w3-center">
<div class="w3-row">
<div class="w3-half">X<br><span id="_x"></span></div>
<div class="w3-half">Y<br><span id="_y"></span></div>
</div>
<button id="_goto-0" class="w3-button w3-light-blue w3-round w3-round-large w3-margin-small w3-block">Origine (0, 0)</button>
<button id="_goto-xy" class="w3-button w3-light-blue w3-round-large w3-margin-small w3-block w3-margin-bottom">
{% trans 'Aller à la base' %}<br>(<span id="_xbase"></span>, <span id="_ybase"></span>)
</button>
<hr>
<button id="_median" class="w3-button w3-teal w3-round-large w3-margin-small w3-block"><i class="fa-solid fa-crosshairs"></i> {% trans 'Axes' %}</button>
<button id="_crop" class="w3-button w3-teal w3-round-large w3-margin-small w3-block w3-margin-bottom"><i class="fa-solid fa-crop"></i> {% trans 'Recadrer' %}</button>
<span>
{% trans 'Rayon' %}: <input id="_crop_radius" type="number" min="100" max="1200" step="1" value="{{ conf.calibration_crop_radius }}" title="{% trans 'Rayon de cadrage' %}"/>
</span>
<hr>
<button id="_test" class="w3-button w3-warning w3-round-large w3-margin-small w3-block">{% trans 'Tester le cirduit' %}</button>
<button id="_halt" class="w3-button w3-red w3-round-large w3-margin-small w3-block"><i class="fa-solid fa-hand"></i> {% trans 'ARRET' %}</button>
</div>
<div class="scanner"><img id="scan-img" class="w3-image"></div>
</div>
<ul id="_debug" class="w3-scroll-y" style="height: 30vh"></ul>
{% endblock %}
{% block js_footer %}
{{ block.super }}
<script src="/static/scanner/js/calibration.js"></script>
<script>
const container = sId("scan-img");
const ws_route = "{{ ws_route }}";
// ---- Point d'entrée ----
(async () => {
const manager = new ScannerManager(container);
const protocol = location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${protocol}://${location.host}/${ws_route}`;
const socket = new MetadataSocket(wsUrl);
socket.setManager(manager);
socket.connect();
manager.registerSocket(socket);
})();
</script>
{% endblock %}
@@ -0,0 +1,6 @@
{% extends 'scanner/base.html' %}
{% block columns %}{% endblock %}
{% block content %}
<iframe width="100%" src="{{ link }}" style="border:none; height:90vh;display:block;"></iframe>
{% endblock %}
@@ -0,0 +1,14 @@
<div class="w3-modal" id="imagemodal" style="z-index: 200;">
<div class="w3-modal-content w3-border w3-dark-medium" style="max-width: 640px">
<button onclick="sId('imagemodal').style.display='none'" class="w3-button w3-round w3-round-xxlarge w3-text-red w3-display-topright w3-xlarge">&#x2716;</button>
<div class="w3-card-4">
<div class="w3-bar w3-dark-xlight" style="padding-right: 4em">
<span class="w3-bar-item" id="imagepreview-uuid"></span>
<span class="w3-bar-item" id="imagepreview-ts"></span>
<span class="w3-bar-item w3-right" id="imagepreview-index"></span>
</div>
<img id="imagepreview" style="width: 100%">
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More