api_g / app.py
DmitrMakeev's picture
Update app.py
4bff661 verified
from flask import Flask, request, jsonify, redirect, url_for, render_template, send_from_directory, send_file, render_template_string
import os
import sqlite3
from datetime import datetime
import pytz
import io
import base64
from dotenv import load_dotenv
import requests
import json
import logging
import uuid
from io import BytesIO
import uuid
from tabulate import tabulate
import numpy as np
from scipy.optimize import linprog
from typing import Dict, Any, Union, List
import itertools
import traceback
import logging
from werkzeug.utils import secure_filename
import globs
from api_logic import api
from urllib.parse import urlencode # Добавлен правильный импорт
load_dotenv()
# Инициализация базы данных
def init_db(db_name):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# Таблица с системными данными (твоя старая таблица)
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_time TEXT,
dey TEXT,
wek TEXT,
v_hid TEXT,
v_min TEXT,
ph TEXT,
ec TEXT,
tS TEXT,
tA TEXT,
hDm TEXT,
sVen TEXT,
onA TEXT,
onB TEXT,
onC TEXT,
nPh TEXT,
nEC TEXT,
nTa TEXT,
nLon TEXT,
nLoff TEXT
)
''')
# **Новая таблица для пользователей бота**
cursor.execute('''
CREATE TABLE IF NOT EXISTS bot_users (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- Уникальный ID
chat_id INTEGER UNIQUE, -- Telegram ID пользователя
created_at TEXT -- Время добавления (ISO формат)
)
''')
conn.commit()
conn.close()
# Глобальные переменные
api_key_sys = os.getenv('api_key') # Берём значение API-ключа
btg_key = os.getenv('btg_key') # Берём значение
btg_id = os.getenv('btg_id') # Берём значение
btg_on = os.getenv('btg_on') # Берём значение
globs.dey = 0
globs.wek = 0
globs.v_hid = 0
globs.v_min = 0
globs.ph = 0
globs.ec = 0
globs.tS = 0
globs.tA = 0
globs.hDm = 0
globs.sVen = 0
globs.onA = 0
globs.onB = 0
globs.onC = 0
globs.ph_eep = 0
globs.ph_on_eep = 0
globs.ec_eep = 0
globs.ec_A_eep = 0
globs.ec_B_eep = 0
globs.ec_C_eep = 0
globs.l_ON_h_eep = 0
globs.l_ON_m_eep = 0
globs.l_OFF_h_eep = 0
globs.l_OFF_m_eep = 0
globs.t_Voz_eep = 0
# Создаем экземпляр Flask-приложения
app = Flask(__name__, template_folder="./")
app.config['DEBUG'] = True
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
# Создаем папку для загрузок если ее нет
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Глобальная переменная для хранения последнего изображения в памяти
latest_image = {"data": None, "filename": None}
# Настроим логирование
logging.basicConfig(level=logging.DEBUG)
# Функция сохранения в базу данных системы автоматизации гидропоники
def save_data_to_db(db_name, data):
try:
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# ✅ Устанавливаем московское время (UTC+3)
moscow_tz = pytz.timezone("Europe/Moscow")
current_time = datetime.now(moscow_tz).strftime('%Y-%m-%d %H:%M:%S')
# Вставляем данные в таблицу
cursor.execute('''
INSERT INTO system_data (
date_time, dey, wek, v_hid, v_min, ph, ec, tS, tA, hDm, sVen, onA, onB, onC, nPh, nEC, nTa, nLon, nLoff
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
current_time, # ✅ Дата и время по Москве
data['dey'], data['wek'], data['v_hid'], data['v_min'], data['ph'], data['ec'],
data['tS'], data['tA'], data['hDm'], data['sVen'], data['onA'], data['onB'],
data['onC'], data['nPh'], data['nEC'], data['nTa'], data['nLon'], data['nLoff']
))
conn.commit()
conn.close()
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
# Маршрут сохранения в базу
@app.route('/sav_db_api', methods=['GET'])
def sav_db_api():
# Инициализируем базу данных
init_db('system_data.db')
# Получаем данные из запроса
data = {
'dey': request.args.get('dey'),
'wek': request.args.get('wek'),
'v_hid': request.args.get('v_hid'),
'v_min': request.args.get('v_min'),
'ph': request.args.get('ph'),
'ec': request.args.get('ec'),
'tS': request.args.get('tS'),
'tA': request.args.get('tA'),
'hDm': request.args.get('hDm'),
'sVen': request.args.get('sVen'),
'onA': request.args.get('onA'),
'onB': request.args.get('onB'),
'onC': request.args.get('onC'),
'nPh': request.args.get('nPh'),
'nEC': request.args.get('nEC'),
'nTa': request.args.get('nTa'),
'nLon': request.args.get('nLon'),
'nLoff': request.args.get('nLoff')
}
# Проверяем, что все необходимые параметры переданы
required_params = ['dey', 'wek', 'v_hid', 'v_min', 'ph', 'ec', 'tS', 'tA', 'hDm', 'sVen', 'onA', 'onB', 'onC', 'nPh', 'nEC', 'nTa', 'nLon', 'nLoff']
for param in required_params:
if data[param] is None:
return jsonify({'status': 'error', 'message': f'Отсутствует параметр: {param}'}), 400
# Сохраняем данные в базу
save_data_to_db('system_data.db', data)
# Возвращаем ответ
return jsonify({'status': 'success', 'message': 'Save OK'})
# Проверка входа на страницы
@app.route('/page_key', methods=['GET'])
def check_api_key():
api_sys_param = request.args.get('api_sys') # Получаем параметр из запроса
if api_sys_param == api_key_sys:
return jsonify({"status": "ok"}), 200 # ✅ Совпадает — отправляем "ok"
else:
return jsonify({"status": "error", "message": "Invalid API key"}), 403 # ❌ Ошибка 403
# Тестовый запрос с установки
@app.route('/test_server', methods=['GET'])
def test_server():
api_key_param = request.args.get('api_sys') # Получаем параметр из запроса
err_ser = 1 if api_key_param == api_key_sys else 0 # Проверяем совпадение ключей
return jsonify(err_ser=err_ser)
@app.route('/test_server_str', methods=['GET'])
def test_server_str():
api_key_param = request.args.get('api_sys')
err_ser = "1" if api_key_param == api_key_sys else "0"
return err_ser # Возвращаем строку "1" или "0"
# Тестовый запрос с установки
@app.route('/btg_teleg', methods=['GET'])
def btg_teleg():
api_key_param = request.args.get('api_sys') # Получаем параметр из запроса
return jsonify(btg_key_ser=btg_key,btg_id_ser=btg_id,btg_on_ser=btg_on)
# Маршрут для вывода всех данных из таблицы
@app.route('/get_all_data', methods=['GET'])
def get_all_data():
try:
conn = sqlite3.connect('system_data.db')
cursor = conn.cursor()
# Выполняем запрос для получения всех данных из таблицы
cursor.execute('SELECT * FROM system_data')
rows = cursor.fetchall()
# Получаем названия столбцов
column_names = [description[0] for description in cursor.description]
# Преобразуем данные в формат JSON
data = []
for row in rows:
data.append(dict(zip(column_names, row)))
conn.close()
# Возвращаем данные в формате JSON
return jsonify(data)
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
# Удаление базы
@app.route('/delite_db', methods=['GET'])
def delete_db():
try:
conn = sqlite3.connect("system_data.db") # Используем вашу БД
cursor = conn.cursor()
# ✅ Удаляем все записи из таблицы
cursor.execute("DELETE FROM system_data")
# ✅ Сбрасываем автоинкрементный счётчик ID (для SQLite)
cursor.execute("DELETE FROM sqlite_sequence WHERE name='system_data'")
conn.commit()
conn.close()
return jsonify({'status': 'ok', 'message': 'База данных успешно очищена'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/plot_week', methods=['GET'])
def plot_week():
try:
# Получаем номер недели из параметров запроса
week_number = request.args.get('week', default=1, type=int)
week_number = max(1, min(30, week_number)) # Ограничиваем диапазон 1-30
# Подключаемся к базе данных
conn = sqlite3.connect('system_data.db')
cursor = conn.cursor()
# Проверяем существование таблицы
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='system_data'")
table_exists = cursor.fetchone()
if not table_exists:
conn.close()
return render_template('plot_week.html', data=None, week_number=week_number, table_exists=False)
# Запрашиваем данные за выбранную неделю
cursor.execute('''
SELECT date_time, dey, ph, ec, tS, tA, hDm, sVen, onA, onB, onC, v_hid, v_min
FROM system_data
WHERE wek = ?
ORDER BY date_time
''', (str(week_number),)) # Приводим week_number к строке, так как wek имеет тип TEXT
rows = cursor.fetchall()
conn.close()
# Если данных нет
if not rows:
return render_template('plot_week.html', data=None, week_number=week_number, table_exists=True)
# Формируем данные для JSON
data = {
'week': week_number,
'dates': [row[0] for row in rows],
'days_of_week': [int(row[1]) if row[1] else 0 for row in rows], # Преобразуем dey в int
'ph': [float(row[2]) if row[2] else 0.0 for row in rows], # pH
'ec': [float(row[3]) if row[3] else 0.0 for row in rows], # EC
'tS': [float(row[4]) if row[4] else 0.0 for row in rows], # Температура раствора
'tA': [float(row[5]) if row[5] else 0.0 for row in rows], # Температура воздуха
'hDm': [float(row[6]) if row[6] else 0.0 for row in rows], # Влажность воздуха
'sVen': [float(row[7]) if row[7] else 0.0 for row in rows], # Обороты вентилятора
'onA': [float(row[8]) if row[8] else 0.0 for row in rows], # Насос A
'onB': [float(row[9]) if row[9] else 0.0 for row in rows], # Насос B
'onC': [float(row[10]) if row[10] else 0.0 for row in rows], # Насос C
'sus': [f"{row[11]}:{row[12]}" if row[11] and row[12] else "0:0" for row in rows] # Объединяем v_hid и v_min
}
# Отправляем данные в HTML-шаблон
return render_template('plot_week.html', data=data, week_number=week_number, table_exists=True)
except Exception as e:
# Логируем ошибку в консоль для отладки
print(f"Ошибка: {str(e)}")
return render_template('plot_week.html', data=None, week_number=week_number, table_exists=True, message=f"Ошибка: {str(e)}")
@app.route("/")
def index():
return flask.render_template('index.html')
@app.route('/online', methods=['GET'])
def online():
return render_template('online.html')
@app.route('/table', methods=['GET'])
def table():
return render_template('table.html')
@app.route('/online_api', methods=['GET'])
def online_api():
# Устанавливаем московское время (UTC+3)
moscow_tz = pytz.timezone("Europe/Moscow")
current_time = datetime.now(moscow_tz)
# Форматируем дату и время отдельно
date = current_time.strftime('%Y-%m-%d') # Например, "2025-03-23"
time = current_time.strftime('%H:%M:%S') # Например, "14:35:42"
return jsonify(
dey=globs.dey,
wek=globs.wek,
v_hid=globs.v_hid,
v_min=globs.v_min,
ph=globs.ph,
ec=globs.ec,
tS=globs.tS,
tA=globs.tA,
hDm=globs.hDm,
sVen=globs.sVen,
rFul=globs.rFul,
rLi=globs.rLi,
rWat=globs.rWat,
rRas=globs.rRas,
rPH=globs.rPH,
rEC=globs.rEC,
rSl=globs.rSl,
rLe=globs.rLe,
alW=globs.alW,
ec_A_eep=globs.ec_A_eep,
ec_B_eep=globs.ec_B_eep,
ec_C_eep=globs.ec_C_eep,
date=date, # Добавляем дату
time=time # Добавляем время
)
@app.route('/settings', methods=['GET'])
def settings():
return render_template('settings.html')
@app.route('/settings_api', methods=['GET'])
def settings_api():
return jsonify(ph_eep=globs.ph_eep,
ph_on_eep=globs.ph_on_eep,
ec_eep=globs.ec_eep,
ec_A_eep=globs.ec_A_eep,
ec_B_eep=globs.ec_B_eep,
ec_C_eep=globs.ec_C_eep,
l_ON_h_eep=globs.l_ON_h_eep,
l_ON_m_eep=globs.l_ON_m_eep,
l_OFF_h_eep=globs.l_OFF_h_eep,
l_OFF_m_eep=globs.l_OFF_m_eep,
t_Voz_eep=globs.t_Voz_eep,
set_st=globs.set_status
)
@app.route('/pH_set', methods=['GET'])
def set_pH_value():
ph_value = request.args.get('value')
globs.ph_set = ph_value
globs.eep_set = 1
return "pH value set successfully"
@app.route('/ph_on_set', methods=['GET'])
def ph_on_value():
ph_on_value = request.args.get('value')
globs.ph_on_set = ph_on_value
globs.eep_set = 2
return "EC value set successfully"
@app.route('/EC_set', methods=['GET'])
def set_EC_value():
ec_value = request.args.get('value')
globs.ec_set = ec_value
globs.eep_set = 3
return "EC value set successfully"
@app.route('/ec_A_set', methods=['GET'])
def ec_A_setValue():
ec_A_setValue = request.args.get('value')
globs.ec_A_set = ec_A_setValue
globs.eep_set = 4
return "EC value set successfully"
@app.route('/ec_B_set', methods=['GET'])
def ec_B_setValue():
ec_B_setValue = request.args.get('value')
globs.ec_B_set = ec_B_setValue
globs.eep_set = 5
return "EC value set successfully"
@app.route('/ec_C_set', methods=['GET'])
def ec_C_setValue():
ec_C_setValue = request.args.get('value')
globs.ec_C_set = ec_C_setValue
globs.eep_set = 6
return "EC value set successfully"
@app.route('/l_ON_set', methods=['GET'])
def l_ON_set():
globs.l_ON_h_set = request.args.get('l_ON_h_set')
globs.l_ON_m_set = request.args.get('l_ON_m_set')
globs.eep_set = 7
return "EC value set successfully"
@app.route('/l_OFF_set', methods=['GET'])
def l_OFF_set():
globs.l_OFF_h_set = request.args.get('l_OFF_h_set')
globs.l_OFF_m_set = request.args.get('l_OFF_m_set')
globs.eep_set = 8
return "EC value set successfully"
@app.route('/t_Voz_eep_set', methods=['GET'])
def t_Voz_eep_set():
t_Voz_eep_set = request.args.get('value')
globs.t_Voz_set = t_Voz_eep_set
globs.eep_set = 9
return "EC value set successfully"
@app.route('/but_start', methods=['GET'])
def but_start():
globs.eep_set = 10
return jsonify(value_set="start")
@app.route('/but_stop', methods=['GET'])
def but_stop():
globs.eep_set = 11
return jsonify(value_set="stop")
@app.route('/but_res', methods=['GET'])
def but_res():
globs.eep_set = 12
return jsonify(value_set="res")
@app.route('/but_sliv', methods=['GET'])
def but_sliv():
globs.eep_set = 13
return jsonify(value_set="sliv")
@app.route("/api", methods=['GET'])
def handle_api():
response = api()
return response
@app.route("/save_db", methods=['GET'])
def handle_save_db():
response = save_db()
return response
@app.route('/set_res')
def set_res():
globs.eep_set = 0
return jsonify(value_set="reset")
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
if not allowed_file(file.filename):
return jsonify({"error": "Invalid file type"}), 400
# Генерируем имя для файла с использованием времени
timestamp = datetime.now().strftime('%Y.%m.%d_%H:%M:%S_')
filename = timestamp + file.filename
save_path = os.path.join(UPLOAD_FOLDER, filename)
# Открываем файл для записи и собираем его части
try:
with open(save_path, 'wb') as f:
while chunk := file.read(1024): # Чтение и запись данных частями
f.write(chunk)
return jsonify({
"message": "File uploaded successfully",
"filename": filename,
"path": save_path
}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/uploads/<filename>', methods=['GET'])
def uploaded_file(filename):
return send_from_directory(UPLOAD_FOLDER, filename)
# 🧠 2. Маршрут: сохраняет файл только в память (BytesIO)
# Маршрут для загрузки файла в память
# Загрузка изображения в память (в виде байтов)
@app.route('/upload_memory', methods=['POST'])
def upload_file_to_memory():
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
if not file.filename.lower().endswith(('.jpg', '.jpeg', '.png')):
return jsonify({"error": "Invalid file type"}), 400
timestamp = datetime.now().strftime('%Y.%m.%d_%H:%M:%S_')
filename = timestamp + file.filename
try:
# Читаем весь файл в байты
file_bytes = file.read()
# Сохраняем в переменную
latest_image["data"] = file_bytes
latest_image["filename"] = filename
return jsonify({
"message": "Image uploaded successfully",
"filename": filename
}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
# Получение последнего изображения
@app.route('/last_image', methods=['GET'])
def get_last_image():
if not latest_image["data"]:
return jsonify({"error": "No image available"}), 404
# Возвращаем через новый BytesIO каждый раз
return send_file(
BytesIO(latest_image["data"]),
mimetype='image/jpeg',
download_name=latest_image["filename"]
)
@app.route('/view_image', methods=['GET'])
def view_image():
return render_template('show_image.html')
@app.route('/nutri_call', methods=['GET'])
def nutri_call():
return render_template('nutri_call.html')
@app.route('/spoller', methods=['GET'])
def spoller():
return render_template('spoller.html')
from tabulate import tabulate
INPUT_DATA = {
"fertilizerConstants": {
"Кальциевая селитра": {"N (NO3-)": 0.11863, "Ca_nitrate": 0.16972},
"Хелат кальция": {"Ca_chelate": 0.10},
"Калий азотнокислый": {"N (NO3-)": 0.13854, "K": 0.36672},
"Аммоний азотнокислый": {"N (NO3-)": 0.17499, "N (NH4+)": 0.17499},
"Сульфат магния": {"Mg": 0.10220, "S": 0.13483},
"Монофосфат калия": {"P": 0.22761, "K": 0.28731},
"Калий сернокислый": {"K": 0.44874, "S": 0.18401}
},
"profileSettings": {
"P": 50, "K": 220, "Mg": 24, "S": 60,
"Ca_nitrate_target": 60,
"Ca_chelate_target": 20,
"NO3_RAT": 4, "TOTAL_NITROG": 125, "liters": 100
}
}
class NutrientCalculator:
def __init__(self, fertilizer_constants, profile_settings, liters, rounding_precision):
self.fertilizers = fertilizer_constants
self.profile = profile_settings
self.volume = liters
self.rounding_precision = rounding_precision
total_parts = self.profile["NO3_RAT"] + 1
self.target = {
'P': self.profile["P"],
'K': self.profile["K"],
'Mg': self.profile["Mg"],
'S': self.profile["S"],
'Ca_nitrate': self.profile["Ca_nitrate_target"],
'Ca_chelate': self.profile["Ca_chelate_target"],
'N (NO3-)': self.profile["TOTAL_NITROG"] * (self.profile["NO3_RAT"] / total_parts),
'N (NH4+)': self.profile["TOTAL_NITROG"] * (1 / total_parts)
}
self.actual = {
'P': 0.0, 'K': 0.0, 'Mg': 0.0, 'S': 0.0,
'Ca_nitrate': 0.0, 'Ca_chelate': 0.0,
'N (NO3-)': 0.0, 'N (NH4+)': 0.0
}
self.results = {fert: {'граммы': 0.0} for fert in self.fertilizers}
def _apply_fertilizer(self, name, element, target_ppm):
if name not in self.fertilizers:
raise KeyError(f"Удобрение '{name}' не найдено!")
content = self.fertilizers[name].get(element, 0)
if content == 0:
print(f"ПРЕДУПРЕЖДЕНИЕ: Удобрение '{name}' не содержит элемент '{element}'")
return
grams = (target_ppm * self.volume) / (content * 1000)
self.results[name]['граммы'] += round(grams, self.rounding_precision)
for el, val in self.fertilizers[name].items():
if el in self.actual:
added_ppm = (grams * val * 1000) / self.volume
self.actual[el] += round(added_ppm, self.rounding_precision)
def _balance_k_s(self):
k_needed = self.target["K"] - self.actual["K"]
s_needed = self.target["S"] - self.actual["S"]
if k_needed > 0 and s_needed > 0:
k_fraction = self.fertilizers["Калий сернокислый"].get("K", 0)
s_fraction = self.fertilizers["Калий сернокислый"].get("S", 0)
if k_fraction == 0 or s_fraction == 0:
return
k_from_k2so4 = min(k_needed, s_needed * k_fraction / s_fraction)
self._apply_fertilizer("Калий сернокислый", "K", k_from_k2so4)
remaining_k = self.target["K"] - self.actual["K"]
if remaining_k > 0:
self._apply_fertilizer("Калий азотнокислый", "K", remaining_k)
def _verify_results(self):
deficits = {}
for el in self.target:
diff = self.target[el] - self.actual[el]
if abs(diff) > 0.1:
deficits[el] = round(diff, self.rounding_precision)
return {
'fertilizers': {k: round(v['граммы'], self.rounding_precision) for k, v in self.results.items() if v['граммы'] > 0},
'actual_profile': {k: round(v, self.rounding_precision) for k, v in self.actual.items()},
'deficits': deficits
}
def calculate(self):
# 1. Вносим хелатный кальций - строго 20 ppm
self._apply_fertilizer("Хелат кальция", "Ca_chelate", self.target["Ca_chelate"])
# 2. Вносим нитратный кальций - строго 60 ppm
self._apply_fertilizer("Кальциевая селитра", "Ca_nitrate", self.target["Ca_nitrate"])
# 3. Остальные элементы
self._apply_fertilizer("Аммоний азотнокислый", "N (NH4+)", self.target["N (NH4+)"])
self._apply_fertilizer("Монофосфат калия", "P", self.target["P"])
self._apply_fertilizer("Сульфат магния", "Mg", self.target["Mg"])
self._balance_k_s()
# 4. Балансируем нитратный азот
no3_needed = self.target["N (NO3-)"] - self.actual["N (NO3-)"]
if no3_needed > 0:
self._apply_fertilizer("Калий азотнокислый", "N (NO3-)", no3_needed)
return self._verify_results()
def generate_report(self, results):
fert_table = []
for name, data in results['fertilizers'].items():
fert_table.append([name, f"{data} г"])
total_ca = self.actual['Ca_nitrate'] + self.actual['Ca_chelate']
ca_diff = total_ca - (self.target['Ca_nitrate'] + self.target['Ca_chelate'])
element_table = [
["P", f"{self.target['P']} ppm", f"{self.actual['P']} ppm"],
["K", f"{self.target['K']} ppm", f"{self.actual['K']} ppm"],
["Mg", f"{self.target['Mg']} ppm", f"{self.actual['Mg']} ppm"],
["S", f"{self.target['S']} ppm", f"{self.actual['S']} ppm"],
["Ca (нитрат)", f"{self.target['Ca_nitrate']} ppm", f"{self.actual['Ca_nitrate']} ppm"],
["Ca (хелат)", f"{self.target['Ca_chelate']} ppm", f"{self.actual['Ca_chelate']} ppm"],
["Ca (общий)", f"80 ppm", f"{total_ca} ppm", f"{ca_diff:+.2f} ppm"],
["N-NO3", f"{self.target['N (NO3-)']} ppm", f"{self.actual['N (NO3-)']} ppm"],
["N-NH4", f"{self.target['N (NH4+)']} ppm", f"{self.actual['N (NH4+)']} ppm"]
]
report = "РЕКОМЕНДУЕМЫЕ УДОБРЕНИЯ:\n"
report += tabulate(fert_table, headers=["Удобрение", "Количество"], tablefmt="grid")
report += "\n\nБАЛАНС ЭЛЕМЕНТОВ:\n"
report += tabulate(element_table,
headers=["Элемент", "Цель", "Факт", "Отклонение"],
tablefmt="grid")
if results['deficits']:
report += "\n\nВНИМАНИЕ: Обнаружены небольшие отклонения:"
for el, diff in results['deficits'].items():
report += f"\n- {el}: не хватает {abs(diff)} ppm"
return report
# Запуск расчета
calculator = NutrientCalculator(
fertilizer_constants=INPUT_DATA["fertilizerConstants"],
profile_settings=INPUT_DATA["profileSettings"],
liters=INPUT_DATA["profileSettings"]["liters"],
rounding_precision=3
)
results = calculator.calculate()
print(calculator.generate_report(results))
@app.route('/calculation', methods=['POST'])
def handle_calculation():
try:
# 1. Получаем и парсим данные
data = request.get_json()
if not data:
return jsonify({"error": "No JSON data received"}), 400
# Логируем входные данные для отладки
print("\n=== ВХОДНЫЕ ДАННЫЕ ===")
print(json.dumps(data, indent=2, ensure_ascii=False))
# 2. Извлекаем основные параметры
fertilizer_constants = data.get("fertilizerConstants", {})
profile_settings = data.get("profileSettings", {})
# Проверяем наличие необходимых полей
if not fertilizer_constants or not profile_settings:
return jsonify({"error": "Missing fertilizerConstants or profileSettings"}), 400
# 3. Извлекаем дополнительные параметры
liters = profile_settings.get("liters", 100) # Объем раствора (литры)
rounding_precision = profile_settings.get("rounding_precision", 3) # Точность округления
# 4. Создаем и запускаем калькулятор
calculator = NutrientCalculator(
fertilizer_constants=fertilizer_constants,
profile_settings=profile_settings,
liters=liters,
rounding_precision=rounding_precision
)
results = calculator.calculate()
# 5. Формируем дополнительные данные
element_contributions = {}
for fert_name in calculator.fertilizers.keys():
grams = calculator.results[fert_name]['граммы']
element_contributions[fert_name] = {}
for element, percent in calculator.fertilizers[fert_name].items():
added_ppm = (grams * percent * 1000) / calculator.volume
element_contributions[fert_name][element] = round(added_ppm, rounding_precision)
# 6. Формируем полный ответ
response = {
"fertilizers": results['fertilizers'],
"actual_profile": results['actual_profile'],
"deficits": results['deficits'],
"total_ppm": results['total_ppm'],
"element_contributions": element_contributions,
"nitrogen_ratios": {
"NH4_RATIO": 1,
"NO3_RATIO": profile_settings.get("NO3_RAT", 0),
"TOTAL_NITROGEN": profile_settings.get("TOTAL_NITROG", 0)
}
}
return jsonify(response), 200
except Exception as e:
error_msg = f"Server error: {str(e)}"
print(error_msg)
return jsonify({
"error": error_msg,
"fertilizers": {},
"actual_profile": {},
"deficits": {},
"element_contributions": {},
"total_ppm": 0
}), 500
# Чат
# Глобальные переменные
messages = []
@app.route('/chat')
def chat_page():
user_id = str(uuid.uuid4())[:8]
return render_template('chat.html', user_id=user_id)
@app.route('/send_message', methods=['POST'])
def send_message():
data = request.json
user_id = data.get('user_id', 'anon')
encrypted_text = data.get('encrypted_text', '')
if not encrypted_text:
return jsonify({'status': 'error', 'message': 'Empty encrypted message'})
message = {
'id': len(messages) + 1,
'user_id': user_id,
'encrypted_text': encrypted_text,
'timestamp': datetime.now().strftime("%H:%M:%S"),
'username': f"User_{user_id}"
}
messages.append(message)
if len(messages) > 200:
messages.pop(0)
return jsonify({'status': 'success'})
@app.route('/get_messages')
def get_messages():
return jsonify({'messages': messages})
@app.route('/get_new_messages')
def get_new_messages():
last_id = request.args.get('last_id', 0, type=int)
new_messages = [m for m in messages if m['id'] > last_id]
return jsonify({'messages': new_messages})
@app.route('/clear_chat', methods=['POST'])
def clear_chat():
global messages
messages.clear()
return jsonify({'status': 'success', 'message': 'Chat cleared'})
# Паттерны в виде JSON
PATTERNS = {
"pattern1": [
"577416161737624147874371687965675387455166255764767112646",
"35257777811486552662718756762243826291673871341244823811",
"8773555692535217838989632448467218821741268475368315292",
"651811262788738622888695683314939713825385323895246722",
"26992388967612584177565252645433784217824855285761494",
"8692527864473743585322777819976163638616341714347544",
"562779651821127844854955691974779992577975885772398",
"28957626913239638349451261172257992735773474359538",
"1853488614553692274496387289473792918351722785582",
"948737575918962492846926918421172219286894964141",
"43611333519868642131628619363289431215584461555",
"7972466861855516344781571399518474336143817611",
"779613557941167978269638439569322769757298472",
"57674813745274776186692373526354946733928329",
"3442394129792254795362511878989441416321252",
"786534532772479275898762966888485557953377",
"65287985959627293488648263577334113758615",
"2716784555689923737513189835167524134576",
"987463911258925111364498828674376547934",
"86219312374827622491848711542714292737",
"5831343512319484641933682696985622911",
"424477863541433115136951866684284212",
"66825659895576426649656953353613633",
"3517225885134168314622658688974996",
"868947474647575245184824557872496",
"55842222112333769693316913669646",
"1436444323566146663647614936611",
"579188755823751339912475439372",
"37197631415136463913623973319",
"1817494556649119314985371641",
"998244912314121345484818715",
"98168413545533479933399686",
"8975354899186727936639655",
"873889389195499739393621",
"61278328115949713333983",
"7396251926544784666382",
"136876128298263133921",
"49564731128189446323",
"4521214231998481955",
"973335654198339151",
"71668229518263166",
"8735142569189473",
"618656726198421",
"79522498718363",
"7574648689299",
"332113558229",
"65324814142",
"2856395556",
"142935112",
"56238623"
],
"pattern2": [
"566464378915756256634131852256166",
"23111716816332872397544947472773",
"5422887597965169537398442229951",
"964176357762676581138386442956",
"61584983548844249242225186252",
"7643482893738664266447695877",
"417731183112531683182465465",
"58514292423784752491612912",
"4465622665163237641773213",
"812284832679551415851534",
"93413325847516556446687",
"3754657432367212181356",
"139123275594933399482",
"43135593154436639431",
"7448153469879393474",
"283968716867333722",
"12365687554166194",
"3592256319573714",
"852472941531185",
"47629245684294",
"2482269253624",
"631486278986",
"94535896885",
"4988486574",
"487335232",
"36168755"
],
"pattern3": [
"6874371515144176932462356166320254753",
"562718666658584635618582773952279238",
"28989533324443198279441951357497252",
"1888586656887418197485156483247977",
"977445322576259917234662132562775",
"75289854734875918957138345728953",
"3718849217363519853842279391858",
"189734238199861848236497331944",
"98717652919857933159147164148",
"8688427211843736465152871553",
"557369932937119112667168618",
"13196935231821123834875579",
"4416638754913235227363137",
"857392639414558749199441",
"43132893455914624119485",
"7445183791515186521434",
"289692171666695273577",
"18662388733365791835",
"9538527616692371928",
"582479477362518121",
"41627425198769933",
"5789267618646936",
"368284479511639",
"95113827562793",
"5624219328973",
"286631351871",
"15394486968",
"6834835665",
"527328232",
"79151155"
]
}
def generate_mandala_html(pattern_array, container_id="mandala-container"):
"""Генерирует HTML код мандалы идентично JavaScript версии"""
def get_color_for_digit(digit):
color_map = {
'1': 'yellow', '2': 'blue', '3': 'turquoise', '4': 'pink',
'5': 'orange', '6': 'violet', '7': 'red', '8': 'dark_blue', '9': 'green'
}
return color_map.get(digit, 'white')
html_parts = [f'<div class="hexagon size-16" id="{container_id}">']
for side in range(1, 7):
html_parts.append(f'<div class="side side-{side}">')
for line in range(16):
if line >= len(pattern_array):
break
pattern_line = pattern_array[line]
rombs_count = 16 - line
html_parts.append(f'<div class="line line-{line}">')
for i in range(min(rombs_count, len(pattern_line))):
digit = pattern_line[i]
color = get_color_for_digit(digit)
html_parts.append(
f'<div class="romb {color}"><span>{digit}</span></div>'
)
html_parts.append('</div>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
# HTML шаблон с кнопками
MANDALA_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<style>
.button-container {
text-align: center;
margin: 20px 0;
}
.pattern-btn {
padding: 10px 20px;
margin: 0 10px;
font-size: 16px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
}
.pattern-btn:hover {
background-color: #45a049;
}
.pattern-btn.active {
background-color: #2196F3;
}
#mandala-container {
margin: 20px auto;
}
/* Базовые стили для мандалы */
.container_top { padding-top: 30px; }
.details_numbers { margin: 15px 0 0px 0; }
.details_numbers br { clear: both; }
.result { margin: 40px 0 40px 0; }
.one_letter { width: 16px; font-size: 16px; display: inline-block; /* float: left; */ }
.bword { font-weight: bold; }
.smenu { margin: 0 auto 20px auto; width: 100%; }
.btn.disabled{ }
.hid { display: none!important; }
.clear { clear: both; }
.graphs { margin: 20px 0 0 0; }
.graph_numbers { /* float: left; */ }
.graph_colors { /* float: left; margin: 0 40px 0 0;*/}
.well-align { margin:auto; width: 490px; }
.method_light { border-left: 1px #000 solid; border-top: 1px #000 solid; margin: auto;}
.graph_numbers .method_light tr td { width: 18px; }
.method_light tr td { border-right: 1px #000 solid; border-bottom: 1px #000 solid; vertical-align: middle; text-align: center; }
.method_light tr td.decart_gorizont { border-bottom: 3px #000 solid!important; }
.method_light tr td.decart_vertical { border-right: 3px #000 solid!important; }
.graph_colors .method_light { border-left: 1px #000 solid; border-top: 1px #000 solid; }
.graph_colors .method_light tr { padding: 0px; margin: 0px; }
.graph_colors .method_light tr td { padding: 0px; margin: 0px; border-right: 1px #000 solid; border-bottom: 1px #000 solid; }
.no-border .method_light, .no-border .method_light tr td {
border: 0px!important;
}
.cell, .cell_noresize { text-align: center; display: block; width: 18px; height: 18px; }
.cell_noresize { border: 1px #ccc solid; }
.white { color: #ffffff; background-color: #ffffff; }
.red { color: #ff0000; background-color: #ff0000; }
.dark_blue { color: #0000ff; background-color: #0000ff; }
.green { color: #336600; background-color: #336600; }
.yellow { color: #ffff00; background-color: #ffff00; }
.blue { color: #0099ff; background-color: #0099ff; }
.turquoise { color: #00ffff; background-color: #00ffff; }
.pink { color: #ff00ff; background-color: #ff00ff; }
.orange { color: #ff9900; background-color: #ff9900; }
.violet { color: #9900ff; background-color: #9900ff; }
@media print {
.white { color: #ffffff!important; background-color: #ffffff!important; }
.red { color: #ff0000!important; background-color: #ff0000!important; }
.dark_blue { color: #0000ff!important; background-color: #0000ff!important; }
.green { color: #336600!important; background-color: #336600!important; }
.yellow { color: #ffff00!important; background-color: #ffff00!important; }
.blue { color: #0099ff!important; background-color: #0099ff!important; }
.turquoise { color: #00ffff!important; background-color: #00ffff!important; }
.pink { color: #ff00ff!important; background-color: #ff00ff!important; }
.orange { color: #ff9900!important; background-color: #ff9900!important; }
.violet { color: #9900ff!important; background-color: #9900ff!important; }
}
.hide, .mob-only { display: none;}
.show { display: block; }
.print { margin: 40px auto; }
.one_color {}
.one_color .cell_noresize { margin-right: 8px; }
.one_color span { float: left; }
.one_color p { clear: both; /* padding: 0 37px; */ }
#word { max-width: calc(100% - 80px); height: 66x; font-size: 20px; }
.navbar-form-own .btn-default { width: 18%; text-transform: uppercase; }
.off { display: none; }
.on { display: block; }
.numerology_details { }
.numerology_details tr td { text-align: center; width: 25px; }
.numerology_details tr td.wc { font-weight: bold; }
.plinks { margin: 10px 0 0 0; padding: 0px; }
.plinks li {
list-style: none;
display: inline-block;
margin: 0 20px 0 0;
padding: 0px;
}
.green_b { background: green!important; }
.red_b { background: red!important; }
.point_number { width: 50px; text-align: center; }
.method_point { margin: auto; }
.graph_numbers .method_point tr td { width: 16px; height: 16px; vertical-align: middle; font-size: 18px; line-height: 16px; }
.method_point tr td { vertical-align: middle; text-align: center; }
.tooltip{
position:absolute;
z-index:1070;
display:block;
margin:0;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-style:normal;
font-weight:400;
line-height:1.5;
text-align:left;
text-align:start;
text-decoration:none;
text-shadow:none;
text-transform:none;
letter-spacing:normal;
word-break:normal;
word-spacing:normal;
white-space:normal;
line-break:auto;
font-size:.875rem;
word-wrap:break-word;
opacity:0
}
.tooltip.show{
opacity:.9
}
.tooltip .arrow{
position:absolute;
display:block;
width:.8rem;
height:.4rem
}
.tooltip .arrow::before{
position:absolute;
content:"";
border-color:transparent;
border-style:solid
}
.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{
padding:.4rem 0
}
.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{
bottom:0
}
.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{
top:0;
border-width:.4rem .4rem 0;
border-top-color:#000
}
.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{
padding:0 .4rem
}
.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{
left:0;
width:.4rem;
height:.8rem
}
.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{
right:0;
border-width:.4rem .4rem .4rem 0;
border-right-color:#000
}
.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{
padding:.4rem 0
}
.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{
top:0
}
.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{
bottom:0;
border-width:0 .4rem .4rem;
border-bottom-color:#000
}
.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{
padding:0 .4rem
}
.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{
right:0;
width:.4rem;
height:.8rem
}
.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{
left:0;
border-width:.4rem 0 .4rem .4rem;
border-left-color:#000
}
.tooltip-inner{
max-width:200px;
padding:.25rem .5rem;
color:#fff;
text-align:center;
background-color:#000;
border-radius:.25rem
}
.glyphicon-exclamation-sign{
margin: 10px 10px 0 10px;
}
.pad0 { padding: 0px!important;}
.pad10 { padding: 20px 5px!important;}
.more-result { margin-top: 10px; text-align: center; }
@media (max-width: 414px){
.mob-only { display: block; }
.well-align { max-width: 100%; }
.point_number { margin-top: 10px; }
.jumbotron .container {
padding: 0px!important;
}
}
.overflow-auto { overflow: auto; width: 100%; }
.nowrap { white-space: nowrap; }
.w100 { text-align: center; margin: 10px 0 0 10px; }
.tor_value { font-size: 10px; }
.nav-pills{
display: flex;
justify-content: center;
align-items: center;
}
.tab-ids li a { font-weight: 600; }
.navbar-form-own {
}
@media (min-width: 1024px){
.tab-ids, .tab-pane .smenu {
display: flex;
justify-content: center;
align-items: center;
}
h1, .h1 {
font-size: 30px!important;
}
}
.romb {
width: 0;
height: 0;
border: 11px solid transparent;
border-bottom: 19px solid transparent; /* #fff */
position: relative;
background-color: transparent;
}
.romb:after {
content: '';
position: absolute;
left: -11px;
top: 19px;
width: 0;
height: 0;
border: 11px solid transparent;
border-top: 19px solid transparent; /*#fff; */
}
.romb span {
position: absolute;
top: 11px;
left: -3px;
/* color: white; */
z-index: 2;
}
.hexagon.courts {
height: 700px;
width: 700px;
margin: 90px auto 20px;
}
.courts .hex {
margin-top: 8px;
width: 16px;
height: 10px;
}
.courts .hex:before {
content: " ";
width: 0; height: 0;
border-bottom: 4px solid var(--hex-color);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
position: absolute;
top: -4px;
}
.courts .hex:after {
content: "";
width: 0;
position: absolute;
bottom: -4px;
border-top: 4px solid var(--hex-color);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
.hexagon.courts .line {
margin-top: -4px!important;
}
.hexagon.courts .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon.courts .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
z-index: 6;
}
.hexagon.courts .side-2 {
transform: rotate(30deg);
position: absolute;
left: 134px;
top: 11px;
z-index: 5;
}
.hexagon.courts .side-3 {
transform: rotate(90deg);
position: absolute;
left: 188px;
top: 133px;
z-index: 4;
}
.hexagon.courts .side-4 {
transform: rotate(150deg);
position: absolute;
left: 118px;
top: 247px;
z-index: 3;
}
.hexagon.courts .side-5 {
transform: rotate(210deg);
position: absolute;
left: -14px;
top: 222px;
z-index: 2;
}
.hexagon.courts .side-6 {
transform: rotate(270deg);
position: absolute;
left: -73px;
top: 114px;
z-index: 1;
}
.hexagon {
position: relative;
/* max-width: 438px;*/
height: 700px;
width: 700px;
margin: 90px auto 20px;
}
.hexagon .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 500px;
margin-top: -11px;
}
.hexagon .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon .side-2 {
transform: rotate(30deg);
position: absolute;
left: 171px; /* меняется */
top: 0px;
}
.hexagon .side-3 {
transform: rotate(90deg);
position: absolute;
left: 256px;
top: 148px;
}
.hexagon .side-4 {
transform: rotate(150deg);
position: absolute;
left: 171px;
top: 296px;
}
.hexagon .side-5 {
transform: rotate(210deg);
position: absolute;
left: 0px;
top: 296px;
}
.hexagon .side-6 {
transform: rotate(270deg);
position: absolute;
left: -85px;
top: 148px;
}
.hexagon.size-6 {
position: relative;
height: 400px;
width: 400px;
margin: 90px auto 20px;
}
.hexagon.size-6 .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 500px;
margin-top: -11px;
}
.hexagon.size-6 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon.size-6 .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon.size-6 .side-2 {
transform: rotate(30deg);
position: absolute;
left: 102px;
top: 11px;
}
.hexagon.size-6 .side-3 {
transform: rotate(90deg);
position: absolute;
left: 114px;
top: 66px;
}
.hexagon.size-6 .side-4 {
transform: rotate(150deg);
position: absolute;
left: 76px;
top: 132px;
}
.hexagon.size-6 .side-5 {
transform: rotate(210deg);
position: absolute;
left: 0px;
top: 132px;
}
.hexagon.size-6 .side-6 {
transform: rotate(270deg);
position: absolute;
left: -38px;
top: 66px;
}
.hexagon.size-8 {
position: relative;
/* max-width: 438px;*/
height: 400px;
width: 700px;
margin: 90px auto 20px;
}
.hexagon.size-8 .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 500px;
margin-top: -11px;
}
.hexagon.size-8 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon.size-8 .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon.size-8 .side-2 {
transform: rotate(30deg);
position: absolute;
left: 95px;
top: 0px;
}
.hexagon.size-8 .side-3 {
transform: rotate(90deg);
position: absolute;
left: 143px;
top: 82px;
}
.hexagon.size-8 .side-4 {
transform: rotate(150deg);
position: absolute;
left: 95px;
top: 164px;
}
.hexagon.size-8 .side-5 {
transform: rotate(210deg);
position: absolute;
left: 0px;
top: 164px;
}
.hexagon.size-8 .side-6 {
transform: rotate(270deg);
position: absolute;
left: -48px;
top: 82px;
}
.hexagon.size-12 {
position: relative;
height: 580px;
width: 580px;
margin: 90px auto 20px;
}
.hexagon.size-12 .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 500px;
margin-top: -11px;
}
.hexagon.size-12 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon.size-12 .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon.size-12 .side-2 {
transform: rotate(30deg);
position: absolute;
left: 133px;
top: 0px;
}
.hexagon.size-12 .side-3 {
transform: rotate(90deg);
position: absolute;
left: 200px;
top: 115px;
}
.hexagon.size-12 .side-4 {
transform: rotate(150deg);
position: absolute;
left: 133px;
top: 230px;
}
.hexagon.size-12 .side-5 {
transform: rotate(210deg);
position: absolute;
left: 0px;
top: 230px;
}
.hexagon.size-12 .side-6 {
transform: rotate(270deg);
position: absolute;
left: -67px;
top: 115px;
}
.hexagon.courts.size-12 {
position: relative;
height: 580px;
width: 580px;
margin: 90px auto 20px;
}
.hexagon.courts.size-12 .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 500px;
margin-top: -11px;
}
.hexagon.courts.size-12 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon.courts.size-12 .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon.courts.size-12 .side-2 {
transform: rotate(30deg);
position: absolute;
left: 77px;
top: 0px;
}
.hexagon.courts.size-12 .side-3 {
transform: rotate(90deg);
position: absolute;
left: 115px;
top: 68px;
}
.hexagon.courts.size-12 .side-4 {
transform: rotate(150deg);
position: absolute;
left: 76px;
top: 135px;
}
.hexagon.courts.size-12 .side-5 {
transform: rotate(210deg);
position: absolute;
left: -1px;
top: 134px;
}
.hexagon.courts.size-12 .side-6 {
transform: rotate(270deg);
position: absolute;
left: -39px;
top: 67px;
}
.hexagon.courts.size-16 .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon.courts.size-16 .side-2 {
transform: rotate(30deg);
position: absolute;
left: 104px;
top: 0px;
}
.hexagon.courts.size-16 .side-3 {
transform: rotate(90deg);
position: absolute;
left: 158px;
top: 90px;
}
.hexagon.courts.size-16 .side-4 {
transform: rotate(150deg);
position: absolute;
left: 104px;
top: 180px;
}
.hexagon.courts.size-16 .side-5 {
transform: rotate(210deg);
position: absolute;
left: 2px;
top: 180px;
}
.hexagon.courts.size-16 .side-6 {
transform: rotate(270deg);
position: absolute;
left: -52px;
top: 92px;
}
.octagon.size-16 {
width: 700px;
height: 700px;
}
.hexagon.size-24 {
position: relative;
/* max-width: 438px;*/
height: 860px;
width: 700px;
margin: 130px auto 130px auto;
}
.hexagon.size-24 .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 500px;
margin-top: -11px;
}
.hexagon.size-24 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.hexagon.size-24 .side-1 {
transform: rotate(-30deg);
position: absolute;
left: 0px;
top: 0px;
}
.hexagon.size-24 .side-2 {
transform: rotate(30deg);
position: absolute;
left: 247px;
top: 0px;
}
.hexagon.size-24 .side-3 {
transform: rotate(90deg);
position: absolute;
left: 371px;
top: 214px;
}
.hexagon.size-24 .side-4 {
transform: rotate(150deg);
position: absolute;
left: 247px;
top: 428px;
}
.hexagon.size-24 .side-5 {
transform: rotate(210deg);
position: absolute;
left: 0px;
top: 427px;
}
.hexagon.size-24 .side-6 {
transform: rotate(270deg);
position: absolute;
left: -124px;
top: 213px;
}
.octagon {
position: relative;
/* max-width: 438px;*/
height: 580px;
width: 580px;
margin: 65px auto 65px;
}
.octagon .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.octagon .side-1 {
transform: rotate(-22.5deg);
position: absolute;
left: 0px;
top: 0px;
}
.octagon .side-2 {
transform: rotate(22.5deg);
position: absolute;
left: 150px; /* меняется */
top: 0px;
}
.octagon .side-3 {
transform: rotate(67.5deg);
position: absolute;
left: 256px;
top: 106px;
}
.octagon .side-4 {
transform: rotate(112.5deg);
position: absolute;
left: 256px;
top: 256px;
}
.octagon .side-5 {
transform: rotate(157.5deg);
position: absolute;
left: 150px;
top: 362px;
}
.octagon .side-6 {
transform: rotate(202.5deg);
position: absolute;
left: 0px;
top: 362px;
}
.octagon .side-7 {
transform: rotate(247.5deg);
position: absolute;
left: -106px;
top: 256px;
}
.octagon .side-8 {
transform: rotate(292.5deg);
position: absolute;
left: -106px;
top: 106px;
}
.octagon.size-6 {
position: relative;
/* max-width: 438px;*/
height: 280px;
width: 580px;
margin: 65px auto 65px;
}
.octagon.size-6 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.octagon.size-6 .side-1 {
transform: rotate(-22.5deg);
position: absolute;
left: 0px;
top: 0px;
}
.octagon.size-6 .side-2 {
transform: rotate(22.5deg);
position: absolute;
left: 66px;
top: 0px;
}
.octagon.size-6 .side-3 {
transform: rotate(67.5deg);
position: absolute;
left: 113px;
top: 47px;
}
.octagon.size-6 .side-4 {
transform: rotate(112.5deg);
position: absolute;
left: 112px;
top: 114px;
}
.octagon.size-6 .side-5 {
transform: rotate(157.5deg);
position: absolute;
left: 66px;
top: 161px;
}
.octagon.size-6 .side-6 {
transform: rotate(202.5deg);
position: absolute;
left: 0px;
top: 161px;
}
.octagon.size-6 .side-7 {
transform: rotate(247.5deg);
position: absolute;
left: -47px;
top: 114px;
}
.octagon.size-6 .side-8 {
transform: rotate(292.5deg);
position: absolute;
left: -47px;
top: 47px;
}
.octagon.size-8 {
position: relative;
/* max-width: 438px;*/
height: 380px;
width: 580px;
margin: 65px auto 65px;
}
.octagon.size-8 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.octagon.size-8 .side-1 {
transform: rotate(-22.5deg);
position: absolute;
left: 0px;
top: 0px;
}
.octagon.size-8 .side-2 {
transform: rotate(22.5deg);
position: absolute;
left: 83px;
top: 0px;
}
.octagon.size-8 .side-3 {
transform: rotate(67.5deg);
position: absolute;
left: 142px;
top: 59px;
}
.octagon.size-8 .side-4 {
transform: rotate(112.5deg);
position: absolute;
left: 142px;
top: 142px;
}
.octagon.size-8 .side-5 {
transform: rotate(157.5deg);
position: absolute;
left: 83px;
top: 201px;
}
.octagon.size-8 .side-6 {
transform: rotate(202.5deg);
position: absolute;
left: 0px;
top: 201px;
}
.octagon.size-8 .side-7 {
transform: rotate(247.5deg);
position: absolute;
left: -59px;
top: 142px;
}
.octagon.size-8 .side-8 {
transform: rotate(292.5deg);
position: absolute;
left: -59px;
top: 59px;
}
.octagon.size-12 {
position: relative;
/* max-width: 438px;*/
height: 600px;
width: 600px;
margin: 65px auto 65px;
}
.octagon.size-12 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.octagon.size-12 .side-1 {
transform: rotate(-22.5deg);
position: absolute;
left: 20px;
top: 24px;
}
.octagon.size-12 .side-2 {
transform: rotate(22.5deg);
position: absolute;
left: 136px;
top: 25px;
}
.octagon.size-12 .side-3 {
transform: rotate(67.5deg);
position: absolute;
left: 220px;
top: 106px;
}
.octagon.size-12 .side-4 {
transform: rotate(112.5deg);
position: absolute;
left: 220px;
top: 223px;
}
.octagon.size-12 .side-5 {
transform: rotate(157.5deg);
position: absolute;
left: 136px;
top: 304px;
}
.octagon.size-12 .side-6 {
transform: rotate(202.5deg);
position: absolute;
left: 20px;
top: 305px;
}
.octagon.size-12 .side-7 {
transform: rotate(247.5deg);
position: absolute;
left: -63px;
top: 223px;
}
.octagon.size-12 .side-8 {
transform: rotate(292.5deg);
position: absolute;
left: -63px;
top: 106px;
}
.octagon.size-24 {
position: relative;
/* max-width: 438px;*/
height: 1050px;
width: 580px;
margin: 65px auto 65px;
}
.octagon.size-24 .side {
display: inline-block;
margin: 0px;
padding: 0px;
}
.octagon.size-24 .side-1 {
transform: rotate(-22.5deg);
position: absolute;
left: -20px;
top: 0px;
}
.octagon.size-24 .side-2 {
transform: rotate(22.5deg);
position: absolute;
left: 196px;
top: 0px;
}
.octagon.size-24 .side-3 {
transform: rotate(67.5deg);
position: absolute;
left: 349px;
top: 153px;
}
.octagon.size-24 .side-4 {
transform: rotate(112.5deg);
position: absolute;
left: 349px;
top: 368px
}
.octagon.size-24 .side-5 {
transform: rotate(157.5deg);
position: absolute;
left: 196px;
top: 521px;
}
.octagon.size-24 .side-6 {
transform: rotate(202.5deg);
position: absolute;
left: -20px;
top: 521px;
}
.octagon.size-24 .side-7 {
transform: rotate(247.5deg);
position: absolute;
left: -172px;
top: 368px;
}
.octagon.size-24 .side-8 {
transform: rotate(292.5deg);
position: absolute;
left: -174px;
top: 152px;
}
.octagon .romb {
width: 0;
height: 0;
border: 9px solid transparent;
border-bottom: 21.8px solid transparent; /* #fff */
position: relative;
background-color: transparent;
}
.octagon .romb:after {
content: '';
position: absolute;
left: -9px;
top: 21.8px;
width: 0;
height: 0;
border: 9px solid transparent;
border-top: 21.8px solid transparent; /*#fff; */
}
.octagon .romb span {
position: absolute;
top: 9px;
left: -5px;
/* color: white; */
z-index: 2;
}
.octagon .line {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 400px;
margin-top: -9px;
}
:root {
--hex-color: transparent;
--hex-border-color: #000;
--dark_blue-color: #0000ff;
--red-color: #ff0000;
}
.romb.white { color: #ffffff; border-bottom-color: #ffffff; }
.romb.white:after { color: #ffffff; border-top-color: #ffffff; }
.romb.red{ color: #ff0000; border-bottom-color: #ff0000; }
.romb.red:after { color: #ff0000; border-top-color: #ff0000; }
.romb.dark_blue { color: #0000ff; border-bottom-color: #0000ff; }
.romb.dark_blue:after { color: #0000ff; border-top-color: #0000ff; }
.romb.green { color: #336600; border-bottom-color: #336600; }
.romb.green:after { color: #336600; border-top-color: #336600; }
.romb.yellow { color: #ffff00; border-bottom-color: #ffff00; }
.romb.yellow:after { color: #ffff00; border-top-color: #ffff00; }
.romb.blue { color: #0099ff; border-bottom-color: #0099ff; }
.romb.blue:after { color: #0099ff; border-top-color: #0099ff; }
.romb.turquoise { color: #00ffff; border-bottom-color: #00ffff; }
.romb.turquoise:after { color: #00ffff; border-top-color: #00ffff; }
.romb.pink { color: #ff00ff; border-bottom-color: #ff00ff; }
.romb.pink:after { color: #ff00ff; border-top-color: #ff00ff; }
.romb.orange { color: #ff9900; border-bottom-color: #ff9900; }
.romb.orange:after { color: #ff9900; border-top-color: #ff9900; }
.romb.violet { color: #9900ff; border-bottom-color: #9900ff; }
.romb.violet:after { color: #9900ff; border-top-color: #9900ff; }
.hex.white { background-color: #ffffff; color:#ffffff; }
.hex.white:before { border-bottom-color: #ffffff; }
.hex.white:after { border-top-color: #ffffff;}
.hex.red { background-color: #ff0000; color:#ff0000; }
.hex.red:before { border-bottom-color: #ff0000; }
.hex.red:after { border-top-color: #ff0000;}
.hex.dark_blue { background-color: #0000ff; color:#0000ff; }
.hex.dark_blue:before { border-bottom-color: #0000ff; }
.hex.dark_blue:after { border-top-color: #0000ff;}
.hex.green { background-color: #336600; color:#336600; }
.hex.green:before { border-bottom-color: #336600; }
.hex.green:after { border-top-color: #336600;}
.hex.yellow { background-color: #ffff00; color:#ffff00; }
.hex.yellow:before { border-bottom-color: #ffff00; }
.hex.yellow:after { border-top-color: #ffff00;}
.hex.blue { background-color: #0099ff; color:#0099ff; }
.hex.blue:before { border-bottom-color: #0099ff; }
.hex.blue:after { border-top-color: #0099ff;}
.hex.turquoise { background-color: #00ffff; color:#00ffff; }
.hex.turquoise:before { border-bottom-color: #00ffff; }
.hex.turquoise:after { border-top-color: #00ffff;}
.hex.pink { background-color: #ff00ff; color:#ff00ff; }
.hex.pink:before { border-bottom-color: #ff00ff; }
.hex.pink:after { border-top-color: #ff00ff;}
.hex.orange { background-color: #ff9900; color:#ff9900; }
.hex.orange:before { border-bottom-color: #ff9900; }
.hex.orange:after { border-top-color: #ff9900;}
.hex.violet { background-color: #9900ff; color:#9900ff; }
.hex.violet:before { border-bottom-color: #9900ff; }
.hex.violet:after { border-top-color: #9900ff;}
.hex {
margin-top: 10px;
width: 18px;
height: 10px;
background-color: var(--hex-color);
position: relative;
color: #fff;
/* transform: rotate(90deg); */
}
.hex span {
position: absolute;
top: -2px;
left: 7px;
z-index: 2;
font-size: 10px;
/* transform: rotate(-90deg); */
}
.hex:before {
content: " ";
width: 0; height: 0;
border-bottom: 5px solid var(--hex-color);
border-left: 9px solid transparent;
border-right: 9px solid transparent;
position: absolute;
top: -5px;
}
.hex:after {
content: "";
width: 0;
position: absolute;
bottom: -5px;
border-top: 5px solid var(--hex-color);
border-left: 9px solid transparent;
border-right: 9px solid transparent;
}
.cards_start {
display: flex;
}
/*
.card {
margin-left: -170px;
}
*/
.owl-carousel { -ms-touch-action: pan-y; touch-action: pan-y; }
.owl-carousel .owl-item img { max-width: 100%; width: auto!important; }
.cards_start.owl-carousel .owl-stage-outer { overflow: inherit; }
.scene {
width: 220px;
height: 321px;
perspective: 600px;
margin-left: -120px;
}
.scene:first-child { margin-left: 0px; }
.card {
width: 100%;
height: 100%;
line-height: 321px;
transition: transform 1s;
transform-style: preserve-3d;
cursor: pointer;
position: relative;
border: 1px solid #CCC;
box-shadow: 0 0 10px rgba(0,0,0,0.8);
text-align: center;
}
.card img { max-width: 221px; }
.scene-active { width: 381px; height: 551px; margin-top: -110px;}
.scene-active .card img { max-width: 381px; }
.card.is-flipped {
transform: rotateY(180deg);
}
.card__face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.card__face--front {
}
.card__face--back {
transform: rotateY(180deg);
}
.rekl { text-align: center; }
.lazy { margin: auto; vertical-align: middle; }
.form-inputs {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
}
.send_build {
margin: 15px 0 0 0;
text-transform: uppercase;
width: 100%!important;
}
.send_build_mobile { display: none;
}
.send_build_web { display: block; }
.width-100 { max-width: 100%!important; }
.more-save i img { max-height: 18px; }
@media (max-width: 768px){
h1 { font-size: 24px!important; }
h2 { font-size: 20px!important; }
h3 { font-size: 16px!important; }
#word { /* width: 100%; */ max-width: calc(100% - 80px); }
.send_build_web {
display: none!important;
}
.w100 { padding: 0; margin: 10px 0 0 0; width: 100%; }
.w100 button:first-child { width: 90%; }
.results { overflow: auto; max-width: 100%; }
.navbar-form-own .btn-default { width: 100%; text-transform: uppercase; }
.container { padding: 15px 0px!important; }
.send_build_mobile { display: block; }
.jumbotron {
padding: 5px 10px!important;
}
.center { text-align: center; }
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.one_num {
width: 16px;
height: 16px;
line-height: 16px;
}
.flex-side { display: inline-block; }
.mt-5 { margin-top: 5px; }
.mt-10 { margin-top: 10px; }
.mt-20 { margin-top: 20px; }
@media (max-width: 420px){
.m-block { display: block; }
.m-none { display: none!important; }
.btn { padding: 6px 10px!important;}
}
.center-red { background: #f6f6f6; color: red; padding: 5px 10px; margin: auto; width: 430px; max-width: 100%; }
</style>
</head>
<body>
<div class="button-container">
<button class="pattern-btn active" onclick="switchPattern('pattern1')">Паттерн 1</button>
<button class="pattern-btn" onclick="switchPattern('pattern2')">Паттерн 2</button>
<button class="pattern-btn" onclick="switchPattern('pattern3')">Паттерн 3</button>
</div>
<br><br><br>
<div id="mandala-display">
{{ mandala_html|safe }}
</div>
<script>
function switchPattern(patternName) {
// Обновляем активную кнопку
document.querySelectorAll('.pattern-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Загружаем новый паттерн
fetch('/switch-pattern', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({pattern: patternName})
})
.then(response => response.json())
.then(data => {
document.getElementById('mandala-display').innerHTML = data.mandala_html;
})
.catch(error => console.error('Error:', error));
}
</script>
</body>
</html>
"""
@app.route('/mandala')
def show_mandala():
# Используем паттерн по умолчанию
mandala_html = generate_mandala_html(PATTERNS["pattern1"])
return render_template_string(MANDALA_TEMPLATE, mandala_html=mandala_html)
@app.route('/switch-pattern', methods=['POST'])
def switch_pattern():
data = request.get_json()
pattern_name = data.get('pattern', 'pattern1')
if pattern_name in PATTERNS:
mandala_html = generate_mandala_html(PATTERNS[pattern_name])
return jsonify({'mandala_html': mandala_html})
else:
return jsonify({'error': 'Pattern not found'}), 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))