diff --git a/MyNewMethod b/MyNewMethod new file mode 160000 index 00000000..61879ead --- /dev/null +++ b/MyNewMethod @@ -0,0 +1 @@ +Subproject commit 61879ead318f5fdc155a7b9a10b99ecbbdb1a696 diff --git a/app/blueprint.py b/app/blueprint.py index 199c037b..36af7cbf 100644 --- a/app/blueprint.py +++ b/app/blueprint.py @@ -11,6 +11,7 @@ from app.routes.setting import blp as setting_blueprint from app.routes.simulation import blp as simulation_blueprint from app.routes.receive import blp as receive_blueprint +from app.routes.user_preference import blp as user_preference_blueprint # Register Blueprint @@ -27,3 +28,4 @@ def register_routing(app): api.register_blueprint(auralization_blueprint) api.register_blueprint(setting_blueprint) api.register_blueprint(receive_blueprint) + api.register_blueprint(user_preference_blueprint) diff --git a/app/models/UserPreference.py b/app/models/UserPreference.py new file mode 100644 index 00000000..bc2ed819 --- /dev/null +++ b/app/models/UserPreference.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from sqlalchemy import JSON + +from app.db import db +from app.types import Setting, Status, ResourceType + +class UserPreference(db.Model): + __tablename__ = "user_preferences" + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + settings = db.Column(JSON, nullable=False) + createdAt = db.Column(db.String(), default=datetime.now()) + updatedAt = db.Column(db.String(), default=datetime.now()) diff --git a/app/models/__init__.py b/app/models/__init__.py index ee51907f..0db76af7 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -8,3 +8,4 @@ from app.models.Simulation import Simulation from app.models.SimulationRun import SimulationRun from app.models.Task import Task +from app.models.UserPreference import UserPreference diff --git a/app/models/data/example_models.json b/app/models/data/example_models.json new file mode 100644 index 00000000..4a7bbc7d --- /dev/null +++ b/app/models/data/example_models.json @@ -0,0 +1,10 @@ +[ + { + "id": "1", + "name": "Measurement Room", + "description": "A description of Measurement Room.", + "thumbnailUrl": "/uploads/model_images/MeasurementRoom.png", + "fileName": "MeasurementRoom.obj", + "filePath": "example_models/MeasurementRoom.obj" + } +] \ No newline at end of file diff --git a/app/models/data/example_projects.json b/app/models/data/example_projects.json new file mode 100644 index 00000000..ee36d1bc --- /dev/null +++ b/app/models/data/example_projects.json @@ -0,0 +1,50 @@ +[ + { + "project": { + "name": "Example Project", + "description": "Auto-generated example project via script", + "group": "Example Group" + }, + "model": { + "directory": "example_models", + "fileName": "MeasurementRoom.obj", + "name": "MeasurementRoom", + "imagePath": "uploads/model_images/MeasurementRoom.png", + "imageFileName": "MeasurementRoom.png" + }, + "simulation": { + "name": "Example Simulation", + "description": "Auto-generated simulation", + "simulationMethod": "DE", + "layerIdByMaterialId": {}, + "sources": [{ + "label": "Source 1", + "orderNumber": 1, + "x": 1.5, + "y": 1.5, + "z": 1.2 + }], + "receivers": [ + { + "label": "Receiver 1", + "orderNumber": 1, + "x": 3.0, + "y": 3.0, + "z": 1.0 + } + ], + "solverSettings": { + "simulationSettings": { + "sim_len_type": "edt", + "edt": 35, + "de_ir_length": 0.5, + "de_c0": 343, + "de_lc": 1 + } + } + }, + "material": { + "name": "Acoustic plaster 68 mm thick" + } + } +] \ No newline at end of file diff --git a/app/models/data/user_preferences.json b/app/models/data/user_preferences.json new file mode 100644 index 00000000..44541c61 --- /dev/null +++ b/app/models/data/user_preferences.json @@ -0,0 +1,7 @@ +[ + { + "settings": { + "hideSimulationSettingErrors": false + } + } +] \ No newline at end of file diff --git a/app/routes/model.py b/app/routes/model.py index 7fd9ff54..26115b1a 100644 --- a/app/routes/model.py +++ b/app/routes/model.py @@ -2,7 +2,7 @@ from flask.views import MethodView from flask_smorest import Blueprint, abort -from app.schemas.model_schema import ModelCreateSchema, ModelInfoSchema, ModelSchema, ModelUpdateSchema, ModelUploadImageResponseSchema +from app.schemas.model_schema import ModelCreateSchema, ModelInfoSchema, ModelSchema, ModelUpdateSchema, ModelUploadImageResponseSchema, ModelExampleSchema from app.services import model_service blp = Blueprint("Model", __name__, description="Model API") @@ -22,6 +22,11 @@ class ModelUploadImage(MethodView): def post(self): return model_service.upload_image(request.files) +@blp.route("/models/examples") +class ModelUploadImage(MethodView): + @blp.response(200, ModelExampleSchema(many=True)) + def get(self): + return model_service.get_example_models() @blp.route("/models/") class Model(MethodView): diff --git a/app/routes/user_preference.py b/app/routes/user_preference.py new file mode 100644 index 00000000..f5bb5c83 --- /dev/null +++ b/app/routes/user_preference.py @@ -0,0 +1,24 @@ +from flask.views import MethodView +from flask_smorest import Blueprint + +from app.schemas.user_preference_schema import ( + UserPreferenceSchema, + UserPreferenceUpdateBodySchema, +) +from app.services import user_preference_service + +blp = Blueprint("User Preference", __name__, description="User Preference API") + +@blp.route("/user-preferences") +class UserPreference(MethodView): + @blp.response(200, UserPreferenceSchema(many=True)) + def get(self): + return user_preference_service.get_all_user_preferences() + +@blp.route("/user-preferences/") +class UserPreferenceDetail(MethodView): + @blp.arguments(UserPreferenceUpdateBodySchema) + @blp.response(200, UserPreferenceSchema) + def put(self, body_data, user_preference_id): + result = user_preference_service.update_user_preference(user_preference_id, body_data) + return result diff --git a/app/schemas/model_schema.py b/app/schemas/model_schema.py index 3b557ffa..92e53322 100644 --- a/app/schemas/model_schema.py +++ b/app/schemas/model_schema.py @@ -48,4 +48,13 @@ class ModelUpdateSchema(Schema): name = fields.Str(required=True) class ModelUploadImageResponseSchema(Schema): - imagePath = fields.Str(required=True) \ No newline at end of file + imagePath = fields.Str(required=True) + +class ModelExampleSchema(Schema): + id = fields.Str(required=True) + name = fields.Str(required=True) + description = fields.Str(required=True) + thumbnailUrl = fields.Str(required=True) + fileName = fields.Str(required=True) + filePath = fields.Str(required=True) + modelUrl = fields.Str(required=True) \ No newline at end of file diff --git a/app/schemas/user_preference_schema.py b/app/schemas/user_preference_schema.py new file mode 100644 index 00000000..54dee059 --- /dev/null +++ b/app/schemas/user_preference_schema.py @@ -0,0 +1,13 @@ +from marshmallow import Schema, fields + + +class UserPreferenceSchema(Schema): + id = fields.Integer() + settings = fields.Dict() + createdAt = fields.String() + updatedAt = fields.String() + + +class UserPreferenceUpdateBodySchema(Schema): + settings = fields.Dict() + diff --git a/app/services/model_service.py b/app/services/model_service.py index b2830c95..58af4db6 100644 --- a/app/services/model_service.py +++ b/app/services/model_service.py @@ -2,6 +2,8 @@ import os import uuid import config +import shutil +import json from flask_smorest import abort from werkzeug.utils import secure_filename @@ -9,8 +11,11 @@ from app.db import db from app.models import Model from config import FeatureToggle, DefaultConfig +from config import app_dir from datetime import datetime +from app.services import file_service + # Create logger for this module logger = logging.getLogger(__name__) @@ -127,3 +132,74 @@ def upload_image(files): except Exception as ex: logger.error(f"Error uploading image file: {ex}") abort(500, message=f"Error uploading image file: {ex}") + + +def copy_example_models_to_uploads(): + # 1. Define the path to the example models catalog JSON file + json_path = os.path.join(app_dir, "models", "data", "example_models.json") + + if not os.path.exists(json_path): + logger.error(f"Catalog JSON not found at: {json_path}") + abort(404, "Example models catalog file not found.") + + # 2. Read and parse the JSON data + with open(json_path, "r") as f: + example_models = json.load(f) + + try: + # Ensure the main destination uploads folder exists before starting the loop + os.makedirs(config.DefaultConfig.UPLOAD_FOLDER, exist_ok=True) + + # 3. Loop through each model item in the JSON array + for model_data in example_models: + src_relative_path = model_data.get("filePath") + if not src_relative_path: + logger.warning(f"Model ID {model_data.get('id')} is missing 'filePath'. Skipping.") + continue + + src_absolute_path = os.path.join(config.basedir, src_relative_path) + + # Validate if the source physical file actually exists + if not os.path.exists(src_absolute_path): + logger.error(f"Physical file not found at path: {src_absolute_path}") + abort(404, f"Example file for {model_data['name']} not found.") + + # 4. Extract the file extension (.obj) and the base filename dynamically + _, file_extension = os.path.splitext(src_relative_path) + base_name = os.path.basename(src_relative_path).replace(file_extension, "") + + unique_name = f"{base_name}{file_extension}" + dst_absolute_path = os.path.join(config.DefaultConfig.UPLOAD_FOLDER, unique_name) + + # 6. Execute the physical file copy operation + shutil.copy2(src_absolute_path, dst_absolute_path) + logger.info(f"Successfully copied {model_data['name']} to: {dst_absolute_path}") + + return {"message": "Initial full projects successfully!"} + + except Exception as ex: + logger.error(f"Failed to execute example models copying method! Error: {ex}") + abort(500, f"Failed to process example models: {ex}") + + +def get_example_models(): + json_path = os.path.join(app_dir, "models", "data", "example_models.json") + + if not os.path.exists(json_path): + logger.error(f"Catalog JSON file not found at: {json_path}") + abort(404, "Example models catalog file not found.") + + try: + with open(json_path, "r") as f: + example_models = json.load(f) + + # Dynamically map and build the modelUrl for each item + for model in example_models: + base_url = file_service.upload_dir().rstrip("/") + model["modelUrl"] = f"{base_url}/{model['fileName']}" + + return example_models + + except Exception as ex: + logger.error(f"Failed to retrieve example models! Error: {ex}") + abort(500, "Internal server error while fetching example models.") \ No newline at end of file diff --git a/app/services/project_service.py b/app/services/project_service.py index 3fc09f81..68119725 100644 --- a/app/services/project_service.py +++ b/app/services/project_service.py @@ -1,14 +1,19 @@ import logging import os +import shutil +import uuid import config +import rhino3dm +import json from flask import jsonify from flask_smorest import abort from sqlalchemy import asc from app.db import db -from app.models import Project -from app.services import simulation_service +from app.models import Project, File +from config import app_dir +from app.services import simulation_service, file_service, model_service, geometry_service, mesh_service, material_service from datetime import datetime # Create logger for this module @@ -153,3 +158,179 @@ def update_project_by_group(group, new_group): abort(400, message=f"Can not update! Error: {ex}") return result + + +def create_example_projects(): + projects = get_all_projects() + if len(projects): + return + + logger.info("Inserting initial example projects") + with open(os.path.join(app_dir, "models", "data", "example_projects.json")) as json_projects: + example_projects = json.load(json_projects) + try: + for example_data in example_projects: + # Step 1: Create group + project + logger.info("Step 1: Create project") + + project_init = example_data["project"] + project = create_new_project({ + "name": project_init["name"], + "description": project_init["description"], + "group": project_init["group"], + }) + + # Step 2: Get upload slot + logger.info("Step 2: Get upload slot") + + slot_data = file_service.get_slot() + slot_id = slot_data["id"] + + # Step 3: Upload model file & copy image preview + logger.info("Step 3: Upload model file and copy image preview") + + model_init = example_data["model"] + example_model_path = os.path.join(config.basedir, model_init["directory"], model_init["fileName"]) + unique_name = f"MeasurementRoom_{uuid.uuid4().hex}.obj" + dst_path = os.path.join(config.DefaultConfig.UPLOAD_FOLDER, unique_name) + shutil.copy2(example_model_path, dst_path) + + if "imageFileName" in model_init and model_init["imageFileName"]: + image_filename = os.path.basename(model_init["imageFileName"]) + + src_image_path = os.path.join(config.basedir, model_init["directory"], image_filename) + + target_img_dir = os.path.join(config.DefaultConfig.UPLOAD_FOLDER, "model_images") + + os.makedirs(target_img_dir, exist_ok=True) + + dst_image_path = os.path.join(target_img_dir, image_filename) + + if os.path.exists(src_image_path): + shutil.copy2(src_image_path, dst_image_path) + logger.info(f"Image preview copied successfully to {dst_image_path}") + else: + logger.warning(f"Source image not found at {src_image_path}") + + file_record = File.query.filter_by(slot=slot_id).first() + file_record.fileName = unique_name + db.session.commit() + file_upload_id = file_record.id + + # Step 4: Consume / delete slot + logger.info("Step 4: Consume upload slot") + + file_service.consume(slot_id) + + # Step 5: Geometry check (synchronous) + logger.info("Step 5: Geometry check") + geometry = geometry_service.start_geometry_check_task(file_upload_id) + source_file_id = geometry.outputModelId # .3dm file created by geometry check + + # Step 6: Create model + logger.info("Step 6: Create model") + model = model_service.create_new_model({ + "name": model_init["name"], + "projectId": project.id, + "sourceFileId": source_file_id, + "imagePath": model_init["imagePath"] + }) + + # Step 7: Create simulation + logger.info("Step 7: Create simulation") + + simulation_init = example_data["simulation"] + simulation = simulation_service.create_new_simulation({ + "modelId": model.id, + "name": simulation_init["name"], + "description": simulation_init["description"], + "simulationMethod": simulation_init["simulationMethod"], + "layerIdByMaterialId": {}, + "solverSettings": simulation_init["solverSettings"], + "sources": [], + "receivers": [], + }) + + # Step 8: Add source and receiver + logger.info("Step 8: Add source and receiver") + + sources = [] + for source in simulation_init["sources"]: + sources.append( + { + "id": str(uuid.uuid4()), + "label": source["label"], + "orderNumber": source["orderNumber"], + "x": source["x"], + "y": source["y"], + "z": source["z"], + "isValid": True, + } + ) + + receivers = [] + for receiver in simulation_init["receivers"]: + receivers.append( + { + "id": str(uuid.uuid4()), + "label": receiver["label"], + "orderNumber": receiver["orderNumber"], + "x": receiver["x"], + "y": receiver["y"], + "z": receiver["z"], + "isValid": True, + } + ) + + simulation_service.update_simulation_by_id( + { + "sources": sources, + "receivers": receivers, + "hasBeenEdited": True, + }, + simulation.id, + ) + + # Step 9: Set materials — read material_name from 3DM mesh userStrings + logger.info("Step 9: Set materials") + + material_init = example_data["material"] + layer_map: dict = {} + materials = material_service.get_all_materials() + if materials: + target = next( + (m for m in materials if m.name == material_init["name"]), + materials[0], + ) + material_id = target.id + + tdm_file = file_service.get_file_by_id(source_file_id) + tdm_path = os.path.join(config.DefaultConfig.UPLOAD_FOLDER, tdm_file.fileName) + if os.path.exists(tdm_path): + model_3dm = rhino3dm.File3dm.Read(tdm_path) + for obj in model_3dm.Objects: + if isinstance(obj.Geometry, rhino3dm.Mesh): + stable_id = str(obj.Attributes.Id) + layer_map[stable_id] = material_id + + simulation_service.update_simulation_by_id( + {"layerIdByMaterialId": layer_map, "hasBeenEdited": True}, + simulation.id, + ) + + # Step 10: Create mesh (synchronous) + logger.info("Step 10: Create mesh") + mesh_service.start_mesh_task(model.id) + + # Step 11: Run simulation + logger.info("Step 11: Run simulation") + simulation_service.start_solver_task(simulation.id) + + logger.info("Example project created successfully!") + + except Exception as ex: + db.session.rollback() + logger.error(f"Can not insert initial projects! Error: {ex}") + abort(400, f"Can not insert initial projects! Error: {ex}") + + return {"message": "Initial full projects successfully!"} diff --git a/app/services/user_preference_service.py b/app/services/user_preference_service.py new file mode 100644 index 00000000..aae56760 --- /dev/null +++ b/app/services/user_preference_service.py @@ -0,0 +1,84 @@ +import json +import logging +import os + +from flask_smorest import abort +from sqlalchemy import asc + +from app.db import db +from app.models import UserPreference +from config import app_dir +from datetime import datetime + +# Create logger for this module +logger = logging.getLogger(__name__) + + +def get_all_user_preferences(): + return UserPreference.query.order_by(asc(UserPreference.id)).all() + + +def create_new_user_preference(user_preference_data): + new_user_preference = UserPreference(**user_preference_data) + + try: + db.session.add(new_user_preference) + db.session.commit() + + except Exception as ex: + db.session.rollback() + logger.error(f"Can not create a new user preference: {ex}") + abort(400, f"Can not create a new user preference: {ex}") + + return new_user_preference + +def update_user_preference(user_preference_id, user_preference_data): + user_preference = UserPreference.query.filter_by(id=user_preference_id).first() + if not user_preference: + abort(404, message="User preference doesn't exist, cannot update!") + + try: + user_preference.settings = user_preference_data["settings"] + user_preference.updatedAt = datetime.now() + db.session.commit() + except Exception as ex: + db.session.rollback() + logger.error(f"Can not update! Error: {ex}") + abort(400, message=f"Can not update! Error: {ex}") + + return user_preference + + +def get_user_preference_by_id(user_preference_id): + user_preference = UserPreference.query.filter_by(id=user_preference_id).first() + if not user_preference: + logger.error("User preference with id " + str(user_preference_id) + " does not exists!") + abort(400, "User preference doesn't exists!") + return user_preference + + +def insert_initial_user_preferences(): + user_preferences = get_all_user_preferences() + if len(user_preferences): + return + logger.info("Inserting initial user preferences") + with open(os.path.join(app_dir, "models", "data", "user_preferences.json")) as json_user_preferences: + initial_user_preferences = json.load(json_user_preferences) + try: + new_user_preferences = [] + for user_preference in initial_user_preferences: + new_user_preferences.append( + UserPreference( + settings=user_preference["settings"], + ) + ) + + db.session.add_all(new_user_preferences) + db.session.commit() + + except Exception as ex: + db.session.rollback() + logger.error(f"Can not insert initial user preferences! Error: {ex}") + abort(400, f"Can not insert initial user preferences! Error: {ex}") + + return {"message": "Initial user preferences added successfully!"} diff --git a/example_models/MeasurementRoom.png b/example_models/MeasurementRoom.png new file mode 100644 index 00000000..9ef8d0e9 Binary files /dev/null and b/example_models/MeasurementRoom.png differ diff --git a/manage.py b/manage.py index 6db6685d..a9c85ba3 100644 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ from passlib.hash import pbkdf2_sha256 from app.db import db -from app.services import auralization_service, material_service, setting_service +from app.services import auralization_service, material_service, setting_service, user_preference_service, project_service, model_service from config import DefaultConfig @@ -85,6 +85,9 @@ def create_db(): material_service.insert_initial_materials() auralization_service.insert_initial_audios_examples() setting_service.insert_initial_settings() + user_preference_service.insert_initial_user_preferences() + project_service.create_example_projects() + model_service.copy_example_models_to_uploads() db.session.commit() @@ -97,6 +100,9 @@ def reset_db(): material_service.insert_initial_materials() auralization_service.insert_initial_audios_examples() setting_service.insert_initial_settings() + user_preference_service.insert_initial_user_preferences() + project_service.create_example_projects() + model_service.copy_example_models_to_uploads() db.session.commit() diff --git a/resultsFVM.pkl b/resultsFVM.pkl new file mode 100644 index 00000000..229eed2d Binary files /dev/null and b/resultsFVM.pkl differ